Skip to content

Commit

Permalink
- Support external browser with SAML, OIDC, SocialLogin and custom So…
Browse files Browse the repository at this point in the history
…cialLogins

- Maintain device cookie to prevent sending email every login
  • Loading branch information
frontegg-david committed Nov 20, 2023
1 parent 46b0c17 commit e73f298
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 52 deletions.
31 changes: 15 additions & 16 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,21 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="${frontegg_domain}"
android:pathPrefix="/frontegg"
android:scheme="https" />

</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="${frontegg_domain}"
android:pathPrefix="/oauth"
android:scheme="https" />
<data android:scheme="https"/>
<data android:host="${frontegg_domain}"/>
<data android:pathPrefix="/oauth/account/activate"/>
<data android:scheme="https"/>
<data android:host="${frontegg_domain}"/>
<data android:pathPrefix="/oauth/account/invitation/accept"/>
<data android:scheme="https"/>
<data android:host="${frontegg_domain}"/>
<data android:pathPrefix="/oauth/account/reset-password"/>
<data android:scheme="https"/>
<data android:host="${frontegg_domain}"/>
<data android:pathPrefix="/oauth/account/social/success"/>
<data android:scheme="https"/>
<data android:host="${frontegg_domain}"/>
<data android:pathPrefix="/oauth/account/login/magic-link"/>

</intent-filter>
</activity>
Expand Down
2 changes: 1 addition & 1 deletion android/src/main/java/com/frontegg/android/FronteggAuth.kt
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ class FronteggAuth(
val logoutCookies = getDomainCookie(baseUrl)
val logoutAccessToken = accessToken.value

CookieManager.getInstance().removeAllCookies(null)
// CookieManager.getInstance().removeAllCookies(null)
if (logoutCookies != null &&
logoutAccessToken != null &&
FronteggApp.getInstance().isEmbeddedMode
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.frontegg.android.embedded

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Base64
import android.util.Log
import android.webkit.JavascriptInterface
import com.frontegg.android.utils.AuthorizeUrlGenerator
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import org.json.JSONException
import org.json.JSONObject
import java.net.URL


class FronteggMessage {
lateinit var action: String
lateinit var payload: String
}

class FronteggNativeBridge(val context: Context) {


private fun parseMessage(jsonString: String): FronteggMessage? {
val jsonData = jsonString.toByteArray(Charsets.UTF_8)
return try {
Gson().fromJson(jsonString, FronteggMessage::class.java)
} catch (e: JsonSyntaxException) {
println("Error decoding JSON: ${e.message}")
null
}
}

@JavascriptInterface
fun loginWithSSO(email: String) {
Log.d("FronteggNativeBridge", "loginWithSSO(${email})")
val generatedUrl = AuthorizeUrlGenerator().generate(email)
val authorizationUrl = Uri.parse(generatedUrl.first)
val browserIntent = Intent(Intent.ACTION_VIEW, authorizationUrl)
browserIntent.addCategory(Intent.CATEGORY_BROWSABLE)
context.startActivity(browserIntent)
}

@JavascriptInterface
fun loginWithSocialLogin(socialLoginUrl: String) {
Log.d("FronteggNativeBridge", "loginWithSocialLogin(${socialLoginUrl})")

val directLogin: Map<String, Any> = mapOf(
"type" to "direct",
"data" to socialLoginUrl
)

val generatedUrl = try {
val jsonData = JSONObject(directLogin).toString().toByteArray(Charsets.UTF_8)
val jsonString = Base64.encodeToString(jsonData, Base64.DEFAULT)
AuthorizeUrlGenerator().generate(null, jsonString)
} catch (e: JSONException) {
AuthorizeUrlGenerator().generate()
}

val authorizationUrl = Uri.parse(generatedUrl.first)
val browserIntent = Intent(Intent.ACTION_VIEW, authorizationUrl)
browserIntent.addCategory(Intent.CATEGORY_BROWSABLE)
context.startActivity(browserIntent)
}

@JavascriptInterface
override fun toString(): String {
return "injectedObject"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import com.frontegg.android.FronteggAuth
import com.frontegg.android.utils.AuthorizeUrlGenerator
import com.frontegg.android.utils.Constants
import com.frontegg.android.utils.Constants.Companion.loginRoutes
import com.frontegg.android.utils.Constants.Companion.oauthUrls
import com.frontegg.android.utils.Constants.Companion.socialLoginRedirectUrl
import com.frontegg.android.utils.Constants.Companion.successLoginRoutes
import kotlinx.coroutines.DelicateCoroutinesApi
Expand Down Expand Up @@ -94,13 +93,28 @@ class FronteggWebClient(val context: Context) : WebViewClient() {

enum class OverrideUrlType {
HostedLoginCallback,
SocialLoginRedirectToBrowser,
SocialOauthPreLogin,
loginRoutes,
internalRoutes,
Unknown
}

private fun isSocialLoginPath(string: String): Boolean {
val patterns = listOf(
"^/frontegg/identity/resources/auth/[^/]+/user/sso/default/[^/]+/prelogin$",
"^/identity/resources/auth/[^/]+/user/sso/default/[^/]+/prelogin$"
)

for (pattern in patterns) {
val regex = Regex(pattern)
if (regex.containsMatchIn(string)) {
return true
}
}

return false
}

private fun getOverrideUrlType(url: Uri): OverrideUrlType {
val urlPath = url.path
val hostedLoginCallback = Constants.oauthCallbackUrl(FronteggApp.getInstance().baseUrl);
Expand All @@ -110,9 +124,7 @@ class FronteggWebClient(val context: Context) : WebViewClient() {
}
if (urlPath != null && url.toString().startsWith(FronteggApp.getInstance().baseUrl)) {

if (urlPath.startsWith("/frontegg/identity/resources/auth/v2/user/sso/default")
&& urlPath.endsWith("/prelogin")
) {
if (isSocialLoginPath(urlPath)) {
return OverrideUrlType.SocialOauthPreLogin
}

Expand All @@ -125,14 +137,6 @@ class FronteggWebClient(val context: Context) : WebViewClient() {
}
}

if (oauthUrls.find { u -> url.toString().startsWith(u) } != null) {
if(url.query?.contains("external=true") == true) {
return OverrideUrlType.SocialLoginRedirectToBrowser
}
return OverrideUrlType.Unknown
}


return OverrideUrlType.Unknown
}

Expand All @@ -155,10 +159,9 @@ class FronteggWebClient(val context: Context) : WebViewClient() {
}
return super.shouldOverrideUrlLoading(view, request)
}
OverrideUrlType.SocialLoginRedirectToBrowser -> {
val browserIntent = Intent(Intent.ACTION_VIEW, url)
context.startActivity(browserIntent)
view!!.loadUrl("${FronteggAuth.instance.baseUrl}/oauth/account/login")

OverrideUrlType.Unknown -> {
openExternalBrowser(request.url)
return true
}

Expand Down Expand Up @@ -203,6 +206,7 @@ class FronteggWebClient(val context: Context) : WebViewClient() {
}
super.onLoadResource(view, url)
}

else -> {
super.onLoadResource(view, url)
}
Expand Down Expand Up @@ -251,6 +255,11 @@ class FronteggWebClient(val context: Context) : WebViewClient() {
return true
}

private fun openExternalBrowser(externalLink: Uri) {
val browserIntent = Intent(Intent.ACTION_VIEW, externalLink)
context.startActivity(browserIntent)
}


private fun handleHostedLoginCallback(webView: WebView?, query: String?): Boolean {
Log.d(TAG, "handleHostedLoginCallback received query: $query")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ open class FronteggWebView : WebView {
settings.safeBrowsingEnabled = true

webViewClient = FronteggWebClient(context)

this.addJavascriptInterface(FronteggNativeBridge(context), "FronteggNativeBridge")
}

fun loadOauthAuthorize() {
Expand Down
1 change: 0 additions & 1 deletion android/src/main/java/com/frontegg/android/services/Api.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.frontegg.android.services

import android.util.Log
import android.webkit.CookieManager
import com.frontegg.android.models.AuthResponse
import com.frontegg.android.models.User
import com.frontegg.android.utils.ApiConstants
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,8 @@ class AuthorizeUrlGenerator {
private val TAG = AuthorizeUrlGenerator::class.java.simpleName
}

private var clientId: String
private var baseUrl: String

init {
this.baseUrl = FronteggApp.getInstance().baseUrl
this.clientId = FronteggApp.getInstance().clientId
}
private var clientId: String = FronteggApp.getInstance().clientId
private var baseUrl: String = FronteggApp.getInstance().baseUrl

private fun createRandomString(length: Int = 16): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
Expand All @@ -40,7 +35,7 @@ class AuthorizeUrlGenerator {
}


fun generate(): Pair<String, String> {
fun generate(loginHint:String? = null, loginAction:String? = null): Pair<String, String> {
val nonce = createRandomString()
val codeVerifier = createRandomString()
val codeChallenge = generateCodeChallenge(codeVerifier)
Expand All @@ -60,6 +55,13 @@ class AuthorizeUrlGenerator {
.appendQueryParameter("code_challenge_method", "S256")
.appendQueryParameter("nonce", nonce)

if(loginHint != null){
authorizeUrlBuilder.appendQueryParameter("login_hint", loginHint)
}

if(loginAction != null){
authorizeUrlBuilder.appendQueryParameter("login_direct_action", loginAction)
}

val url = authorizeUrlBuilder.build().toString()
Log.d(TAG, "Generated url: $url")
Expand Down
9 changes: 0 additions & 9 deletions android/src/main/java/com/frontegg/android/utils/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,6 @@ class ApiConstants {
class Constants {

companion object {
val oauthUrls = listOf(
"https://www.facebook.com",
"https://accounts.google.com",
"https://github.com/login/oauth/authorize",
"https://login.microsoftonline.com",
"https://slack.com/openid/connect/authorize",
"https://appleid.apple.com",
"https://www.linkedin.com/oauth/"
)

val successLoginRoutes = listOf(
"/oauth/account/social/success",
Expand Down

0 comments on commit e73f298

Please sign in to comment.