Skip to content

Commit

Permalink
[SDK-3234] Web Auth implementation on Android for login & logout (#17)
Browse files Browse the repository at this point in the history
* chore: prep manifests for Android SDK and include dependency on Auth0.Android

* chore: add VSCode launch targets for Flutter

* chore: add generic extension on map to get value or null

* feat: web auth login on Android

* chore: rename map extension and use in web auth result

* chore: rename expiresAt to expiresIn

* chore: read domain and client ID from method call arguments

* Add Logout functionality

* Add logout tests

* Refactor tests for WebAuth

* Fix broken tests

* Remove unused test file, and module import

* Fix tests

* update tests

* chore: move auth0 config into strings resource file

* chore: make scheme configurable through message channel

* Update auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterWebAuthMethodCallHandler.kt

Co-authored-by: Frederik Prijck <[email protected]>

* chore: use configured scheme for logout

* chore: get Auth0 vars from .env

* chore: copy .env file in CI

* chore: copy .env example for iOS and Android builds

* chore: pass all options through to Android webauth login

* chore: pass scheme through to logout action

* chore: favour comments over TODO()

* chore: convert expiresAt to ISO 8601 string

* Fix web auth tests

* chore: remove flutter ios build step

Co-authored-by: frederikprijck <[email protected]>
Co-authored-by: Frederik <[email protected]>
  • Loading branch information
3 people authored Apr 6, 2022
1 parent 3b2287d commit f4bf846
Show file tree
Hide file tree
Showing 27 changed files with 523 additions and 45 deletions.
8 changes: 6 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ jobs:
working_directory: ./auth0_flutter
- run:
name: Analyze App Facing Package
command: dart analyze
command: |
cp example/.env.example example/.env
dart analyze
working_directory: ./auth0_flutter
- run:
name: Test Platform Interface Package
Expand Down Expand Up @@ -68,7 +70,9 @@ jobs:
working_directory: ./auth0_flutter/example/ios
- run:
name: Build Android Example App
command: flutter build apk
command: |
cp .env.example .env
flutter build apk
working_directory: ./auth0_flutter/example
workflows:
tests:
Expand Down
68 changes: 68 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "auth0_flutter",
"cwd": "auth0_flutter",
"request": "launch",
"type": "dart"
},
{
"name": "auth0_flutter (profile mode)",
"cwd": "auth0_flutter",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "auth0_flutter (release mode)",
"cwd": "auth0_flutter",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "auth0_flutter_platform_interface",
"cwd": "auth0_flutter_platform_interface",
"request": "launch",
"type": "dart"
},
{
"name": "auth0_flutter_platform_interface (profile mode)",
"cwd": "auth0_flutter_platform_interface",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "auth0_flutter_platform_interface (release mode)",
"cwd": "auth0_flutter_platform_interface",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "example",
"cwd": "auth0_flutter/example",
"request": "launch",
"type": "dart"
},
{
"name": "example (profile mode)",
"cwd": "auth0_flutter/example",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "example (release mode)",
"cwd": "auth0_flutter/example",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}
3 changes: 2 additions & 1 deletion auth0_flutter/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ android {
}

defaultConfig {
minSdkVersion 16
minSdkVersion 21
}
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.auth0.android:auth0:2.7.0'
}
21 changes: 20 additions & 1 deletion auth0_flutter/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.auth0.auth0_flutter">
xmlns:tools="http://schemas.android.com/tools"
package="com.auth0.auth0_flutter">
<uses-permission android:name="android.permission.INTERNET" />

<application>
<activity
android:name="com.auth0.android.provider.RedirectActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

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

<data
android:host="${auth0Domain}"
android:pathPrefix="/android/${applicationId}/callback"
android:scheme="${auth0Scheme}" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
package com.auth0.auth0_flutter

import android.content.Context
import android.util.Log
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result


/** Auth0FlutterPlugin */
class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler {
class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private lateinit var webAuthMethodChannel : MethodChannel
private lateinit var authMethodChannel : MethodChannel

private val webAuthCallHandler = Auth0FlutterWebAuthMethodCallHandler()

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
webAuthMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "auth0.com/auth0_flutter/web_auth")
webAuthMethodChannel.setMethodCallHandler(Auth0FlutterWebAuthMethodCallHandler())
webAuthMethodChannel.setMethodCallHandler(webAuthCallHandler)

authMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "auth0.com/auth0_flutter/auth")
authMethodChannel.setMethodCallHandler(Auth0FlutterAuthMethodCallHandler())
Expand All @@ -32,4 +36,20 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler {
webAuthMethodChannel.setMethodCallHandler(null)
authMethodChannel.setMethodCallHandler(null)
}

override fun onAttachedToActivity(binding: ActivityPluginBinding) {
webAuthCallHandler.context = binding.activity
}

override fun onDetachedFromActivityForConfigChanges() {
// Not yet implemented
}

override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
// Not yet implemented
}

override fun onDetachedFromActivity() {
// Not yet implemented
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,128 @@
package com.auth0.auth0_flutter

import android.content.Context
import androidx.annotation.NonNull
import com.auth0.android.Auth0
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.callback.Callback
import com.auth0.android.provider.WebAuthProvider
import com.auth0.android.result.Credentials
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap

class Auth0FlutterWebAuthMethodCallHandler: MethodCallHandler {
class Auth0FlutterWebAuthMethodCallHandler : MethodCallHandler {
private val WEBAUTH_LOGIN_METHOD = "webAuth#login"
private val WEBAUTH_LOGOUT_METHOD = "webAuth#logout"
lateinit var context: Context

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
WEBAUTH_LOGIN_METHOD -> {

val callback = object : Callback<Credentials, AuthenticationException> {
override fun onFailure(exception: AuthenticationException) {
result.error(exception.getCode(), exception.getDescription(), exception);
}

override fun onSuccess(credentials: Credentials) {
// Success! Access token and ID token are presents
val scope = credentials.scope?.split(" ") ?: listOf()

val sdf =
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())

val formattedDate = sdf.format(credentials.expiresAt)

result.success(mapOf(
"accessToken" to "AccessToken",
"idToken" to "IdToken",
"refreshToken" to "RefreshToken",
"userProfile" to mapOf("name" to "John Doe"),
"expiresAt" to "2022-04-05",
"scopes" to listOf("a", "b")
"accessToken" to credentials.accessToken,
"idToken" to credentials.idToken,
"refreshToken" to credentials.refreshToken,
"userProfile" to mapOf<String, String>(),
"expiresAt" to formattedDate,
"scopes" to scope
))
}
}

val logoutCallback = object: Callback<Void?, AuthenticationException> {
override fun onFailure(exception: AuthenticationException) {
result.error(exception.getCode(), exception.getDescription(), exception);
}

override fun onSuccess(res: Void?) {
result.success(null);
}
}

when (call.method) {

WEBAUTH_LOGIN_METHOD -> {
val args = call.arguments as HashMap<*, *>;

val loginBuilder = WebAuthProvider
.login(Auth0(args["clientId"] as String, args["domain"] as String))

val scopes = args.getOrDefault("scopes", arrayListOf<String>()) as ArrayList<*>
if (scopes.isNotEmpty()) {
loginBuilder.withScope(scopes.joinToString(separator = " "))
}

if (args.getOrDefault("audience", null) is String) {
loginBuilder.withAudience(args["audience"] as String)
}

if (args.getOrDefault("redirectUri", null) is String) {
loginBuilder.withRedirectUri(args["redirectUri"] as String)
}

if (args.getOrDefault("organizationId", null) is String) {
loginBuilder.withOrganization(args["organizationId"] as String)
}

if (args.getOrDefault("invitationUrl", null) is String) {
loginBuilder.withInvitationUrl(args["invitationUrl"] as String)
}

if (args.getOrDefault("leeway", null) is Int) {
loginBuilder.withIdTokenVerificationLeeway(args["leeway"] as Int)
}

if (args.getOrDefault("maxAge", null) is Int) {
loginBuilder.withMaxAge(args["maxAge"] as Int)
}

if (args.getOrDefault("issuer", null) is String) {
loginBuilder.withIdTokenVerificationIssuer(args["issuer"] as String)
}

if (args.getOrDefault("scheme", null) is String) {
loginBuilder.withScheme(args["scheme"] as String)
}

if (args.getOrDefault("parameters", hashMapOf<String, Any?>()) is HashMap<*, *>) {
loginBuilder.withParameters(args["parameters"] as HashMap<String, *>)
}

loginBuilder.start(context, callback)
}
WEBAUTH_LOGOUT_METHOD -> {
result.success("Web Auth Logout Success")
val args = call.arguments as HashMap<*, *>;

val logoutBuilder = WebAuthProvider
.logout(Auth0(args["clientId"] as String, args["domain"] as String))

if (args.getOrDefault("scheme", null) is String) {
logoutBuilder.withScheme(args["scheme"] as String)
}

if (args.getOrDefault("returnTo", null) is String) {
logoutBuilder.withReturnToUrl(args["returnTo"] as String)
}

logoutBuilder.start(context, logoutCallback)
}
else -> {
result.notImplemented()
Expand Down
3 changes: 3 additions & 0 deletions auth0_flutter/example/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
AUTH0_DOMAIN=
AUTH0_CLIENT_ID=
AUTH0_CUSTOM_SCHEME=
1 change: 1 addition & 0 deletions auth0_flutter/example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
.buildlog/
.history
.svn/
.env

# IntelliJ related
*.iml
Expand Down
8 changes: 5 additions & 3 deletions auth0_flutter/example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
compileSdkVersion flutter.compileSdkVersion
compileSdk 31

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
Expand All @@ -44,10 +44,12 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.auth0.auth0_flutter_example"
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
minSdkVersion 21
targetSdkVersion 31
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
manifestPlaceholders = [auth0Domain: "@string/com_auth0_domain", auth0Scheme: "@string/com_auth0_scheme"]

}

buildTypes {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.auth0.auth0_flutter_example">
<application
android:label="auth0_flutter_example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
tools:node="merge"
>
<activity
android:name=".MainActivity"
android:exported="true"
Expand All @@ -25,6 +27,7 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="com_auth0_domain">elkdanger.eu.auth0.com</string>
<string name="com_auth0_scheme">demo</string>
</resources>
Loading

0 comments on commit f4bf846

Please sign in to comment.