Skip to content

Commit

Permalink
Add support for running flows (#12)
Browse files Browse the repository at this point in the history
* Regenerate platform code

* Add flow running enabled by platform specific logic

* Implement secure storage on Android and iOS

* PR fixes

* Minor cleanup

* Clear current runner on all errors

* Exposing all of session.dart to include all extensions

* Documentation and other fixes

* Fix documentation indentation

* Update exports and documentation

* Update README

* Use claims from session token instead of refresh

* Fix import for session

* Cleanup

* Merge session extensions

* Fix warning

---------

Co-authored-by: Gil Shapira <gshapira@gmail.com>
  • Loading branch information
itaihanski and shilgapira committed Aug 8, 2023
1 parent 40dd20e commit 6f7c302
Show file tree
Hide file tree
Showing 51 changed files with 2,013 additions and 320 deletions.
144 changes: 128 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ You can read more on the [Descope Website](https://descope.com).

## Features

- [Quickstart](#quickstart)
- [Session Management](#session-management)
- [Authentication Flows](#running-flows)
- Authenticate users using the authentication methods that suit your needs:
- [OTP](#otp-authentication) (one-time password)
- [TOTP](#totp-authentication) (timed one-time password / authenticator app)
Expand All @@ -14,7 +17,6 @@ You can read more on the [Descope Website](https://descope.com).
- [OAuth](#oauth) (social)
- [SSO / SAML](#ssosaml)
- [Passwords](#password-authentication) (unrecommended form of authentication)
- [Session Management](#session-management)

## Quickstart

Expand All @@ -23,10 +25,11 @@ on the [project page](https://app.descope.com/settings/project) in
the Descope Console.

```dart
import 'package:descope_flutter/descope.dart';
import 'package:descope/descope.dart';
// Where your application is being created
// Where your application state is being created
Descope.projectId = '<Your-Project-ID>';
await Descope.sessionManager.loadSession();
```

Authenticate the user in your application by starting one of the
Expand All @@ -44,7 +47,7 @@ Finish the authentication by verifying the OTP code the user entered:
final authResponse = await Descope.otp.verify(method: DeliveryMethod.Email, loginId: "andy@example.com", code: code);
// we create a DescopeSession object that represents an authenticated user session
final session = DescopeSession(authResponse);
final session = DescopeSession.fromAuthenticationResponse(authResponse);
// the session manager automatically takes care of persisting the session
// and refreshing it as needed
Expand Down Expand Up @@ -76,14 +79,13 @@ The `DescopeSessionManager` class is used to manage an authenticated
user session for an application.

The session manager takes care of loading and saving the session as well
as ensuring that it's refreshed when needed.

Once the user completes a sign in flow successfully you should set the
`DescopeSession` object as the active session of the session manager.
as ensuring that it's refreshed when needed. When the user completes a sign
in flow successfully you should set the `DescopeSession` object as the
active session of the session manager.

```dart
final authResponse = await Descope.otp.verify(method: DeliverMethod.Email, loginId: "andy@example.com", code: "123456");
final session = DescopeSession(authResponse);
final session = DescopeSession.fromAuthenticationResponse(authResponse);
Descope.sessionManager.manageSession(session);
```

Expand All @@ -110,15 +112,32 @@ if (sessionJwt != null) {
}
```

When the application is relaunched the `DescopeSessionManager` loads any
existing session automatically, so you can check straight away if there's
an authenticated user.
When the application is relaunched the `DescopeSessionManager` can load the existing
session and you can check straight away if there's an authenticated user.

```dart
await Descope.sessionManager.loadSession();
```

If you prefer to call `loadSession` in your `main()` function, before the platform's
`runApp()` function is called, then you'll need to ensure the widget bindings are
initialized first:

```dart
Descope.projectId = "...";
final session = Descope.sessionManager.session;
if (session != null) {
print("User is logged in: $session");
void main() async {
WidgetsFlutterBinding.ensureInitialized();
Descope.projectId = '...';
await Descope.sessionManager.loadSession();
final session = Descope.sessionManager.session;
if (session != null) {
print("User is logged in: ${session.user}");
}
runApp(
...
);
}
```

Expand All @@ -137,6 +156,99 @@ You can customize how the `DescopeSessionManager` behaves by using
your own `storage` and `lifecycle` objects. See the documentation
for more details.

## Running Flows

We can authenticate users by building and running Flows. Flows are built in the Descope
[flow editor](https://app.descope.com/flows). The editor allows you to easily
define both the behavior and the UI that take the user through their
authentication journey. Read more about it in the Descope
[getting started](https://docs.descope.com/build/guides/gettingstarted/) guide.

### Setup #1: Define and host your flow

Before we can run a flow, it must first be defined and hosted. Every project
comes with predefined flows out of the box. You can customize your flows to suit your needs
and host it. Follow
the [getting started](https://docs.descope.com/build/guides/gettingstarted/) guide for more details.
You can host the flow yourself or leverage Descope's hosted flow page. Read more about it [here](https://docs.descope.com/customize/auth/oidc/#hosted-flow-application).
You can also check out the [auth-hosting repo itself](https://github.com/descope/auth-hosting).

### (Android Only) Setup #2: Enable App Links

Running a flow via the Flutter SDK, when targeting Android, requires setting up [App Links](https://developer.android.com/training/app-links#android-app-links).
This is essential for the SDK to be notified when the user has successfully
authenticated using a flow. Once you have a domain set up and
[verified](https://developer.android.com/training/app-links/verify-android-applinks)
for sending App Links, you'll need to handle the incoming deep links in your app:

#### Define a route to handle the App Link sent at the end of a flow
_this code example demonstrates how app links should be handled - you can customize it to fit your app_
```dart
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (_, __) => const MyHomePage(title: 'Flutter Demo Home Page'), // Your main app
routes: [
GoRoute(
path: 'auth', // This path needs to correspond to the deep link you configured in your manifest - see below
redirect: (context, state) async {
try {
Descope.flow.exchange(state.uri); // Call exchange to complete the flow
} catch (e) {
// Handle errors here
}
return "/"; // This route doesn't display anything but returns the root path where the user will be signed in
},
),
],
),
],
);
```

#### Add a matching Manifest declaration
Read more about the flutter specific `meta-data` tag mentioned here in the [official documentation](https://docs.flutter.dev/ui/navigation/deep-linking).
```xml
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> <!-- exported, singleTop are enabled by default but required for the deep links to work -->

<!-- add the following at the end of the activity tag, after anything you have defined currently -->

<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
<intent-filter android:autoVerify="true"> <!-- autoVerify required for app links -->
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- replace with your host, the path can change must must be reflected when running the flow -->
<!-- the path should correspond with the routing path defined above -->
<data android:scheme="https" android:host="<YOUR_HOST_HERE>" android:path="/auth" />
</intent-filter>
</activity>
```

### Run a Flow

After completing the prerequisite steps, it is now possible to run a flow.
The flow will run in a Chrome [Custom Tab](https://developer.chrome.com/docs/android/custom-tabs/) on Android,
or via [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) on iOS.
Run the flow by calling the flow start function:

```dart
final authResponse = await Descope.flow.start("<URL_FOR_FLOW_IN_SETUP_#1>", deepLinkUrl: "<URL_FOR_APP_LINK_IN_SETUP_#2>");
final session = DescopeSession.fromAuthenticationResponse(authResponse);
Descope.sessionManager.manageSession(session);
```

When running on iOS nothing else is required. When running on Android, `Descope.flow.exchange()` function must be called.
See the [app link setup](#-android-only--setup-2--enable-app-links) for more details.

## Authentication Methods

We can authenticate users by using any combination of the authentication methods
Expand Down
9 changes: 9 additions & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx

This file was deleted.

66 changes: 66 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
group 'com.descope.flutter'
version '1.0-SNAPSHOT'

buildscript {
ext.kotlin_version = '1.8.21'
repositories {
google()
mavenCentral()
}

dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
mavenCentral()
}
}

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
compileSdk 33

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = '1.8'
}

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
test.java.srcDirs += 'src/test/kotlin'
}

defaultConfig {
minSdk 24
targetSdk 33
}

dependencies {
api "androidx.browser:browser:1.5.0"
api "androidx.security:security-crypto:1.0.0"
}

testOptions {
unitTests.all {
useJUnitPlatform()

testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}

Binary file added android/gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
5 changes: 5 additions & 0 deletions android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading

0 comments on commit 6f7c302

Please sign in to comment.