Skip to content

Commit

Permalink
Add support for LoginOptions
Browse files Browse the repository at this point in the history
  • Loading branch information
shilgapira committed Aug 8, 2023
1 parent 27da68f commit cb110f1
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 51 deletions.
65 changes: 44 additions & 21 deletions lib/src/internal/http/descope_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ class DescopeClient extends HttpClient {
});
}

Future<MaskedAddressServerResponse> otpSignIn(DeliveryMethod method, String loginId) {
return post('auth/otp/signin/${method.name}', MaskedAddressServerResponse.decoder, body: {
Future<MaskedAddressServerResponse> otpSignIn(DeliveryMethod method, String loginId, SignInOptions? options) {
return post('auth/otp/signin/${method.name}', MaskedAddressServerResponse.decoder, headers: authorization(options?.refreshJwt), body: {
'loginId': loginId,
"loginOptions": options?.toMap(),
});
}

Future<MaskedAddressServerResponse> otpSignUpIn(DeliveryMethod method, String loginId) {
return post('auth/otp/signup-in/${method.name}', MaskedAddressServerResponse.decoder, body: {'loginId': loginId});
Future<MaskedAddressServerResponse> otpSignUpIn(DeliveryMethod method, String loginId, SignInOptions? options) {
return post('auth/otp/signup-in/${method.name}', MaskedAddressServerResponse.decoder, headers: authorization(options?.refreshJwt), body: {
'loginId': loginId,
"loginOptions": options?.toMap(),
});
}

Future<JWTServerResponse> otpVerify(DeliveryMethod method, String loginId, String code) {
Expand Down Expand Up @@ -58,10 +62,11 @@ class DescopeClient extends HttpClient {
});
}

Future<JWTServerResponse> totpVerify(String loginId, String code) {
return post('auth/totp/verify', JWTServerResponse.decoder, body: {
Future<JWTServerResponse> totpVerify(String loginId, String code, SignInOptions? options) {
return post('auth/totp/verify', JWTServerResponse.decoder, headers: authorization(options?.refreshJwt), body: {
'loginId': loginId,
'code': code,
"loginOptions": options?.toMap(),
});
}

Expand Down Expand Up @@ -124,17 +129,19 @@ class DescopeClient extends HttpClient {
});
}

Future<MaskedAddressServerResponse> magicLinkSignIn(DeliveryMethod method, String loginId, String? uri) {
return post('auth/magiclink/signin/${method.name}', MaskedAddressServerResponse.decoder, body: {
Future<MaskedAddressServerResponse> magicLinkSignIn(DeliveryMethod method, String loginId, String? uri, SignInOptions? options) {
return post('auth/magiclink/signin/${method.name}', MaskedAddressServerResponse.decoder, headers: authorization(options?.refreshJwt), body: {
'loginId': loginId,
'uri': uri,
"loginOptions": options?.toMap(),
});
}

Future<MaskedAddressServerResponse> magicLinkSignUpOrIn(DeliveryMethod method, String loginId, String? uri) {
return post('auth/magiclink/signup-in/${method.name}', MaskedAddressServerResponse.decoder, body: {
Future<MaskedAddressServerResponse> magicLinkSignUpOrIn(DeliveryMethod method, String loginId, String? uri, SignInOptions? options) {
return post('auth/magiclink/signup-in/${method.name}', MaskedAddressServerResponse.decoder, headers: authorization(options?.refreshJwt), body: {
'loginId': loginId,
'uri': uri,
"loginOptions": options?.toMap(),
});
}

Expand Down Expand Up @@ -171,17 +178,19 @@ class DescopeClient extends HttpClient {
});
}

Future<EnchantedLinkServerResponse> enchantedLinkSignIn(String loginId, String? uri) {
return post('auth/enchantedlink/signin/email', EnchantedLinkServerResponse.decoder, body: {
Future<EnchantedLinkServerResponse> enchantedLinkSignIn(String loginId, String? uri, SignInOptions? options) {
return post('auth/enchantedlink/signin/email', EnchantedLinkServerResponse.decoder, headers: authorization(options?.refreshJwt), body: {
'loginId': loginId,
'uri': uri,
"loginOptions": options?.toMap(),
});
}

Future<EnchantedLinkServerResponse> enchantedLinkSignUpOrIn(String loginId, String? uri) {
return post('auth/enchantedlink/signup-in/email', EnchantedLinkServerResponse.decoder, body: {
Future<EnchantedLinkServerResponse> enchantedLinkSignUpOrIn(String loginId, String? uri, SignInOptions? options) {
return post('auth/enchantedlink/signup-in/email', EnchantedLinkServerResponse.decoder, headers: authorization(options?.refreshJwt), body: {
'loginId': loginId,
'uri': uri,
"loginOptions": options?.toMap(),
});
}

Expand All @@ -201,10 +210,12 @@ class DescopeClient extends HttpClient {

// OAuth

Future<OAuthServerResponse> oauthStart(OAuthProvider provider, String? redirectUrl) {
return post('auth/oauth/authorize', OAuthServerResponse.decoder, params: {
Future<OAuthServerResponse> oauthStart(OAuthProvider provider, String? redirectUrl, SignInOptions? options) {
return post('auth/oauth/authorize', OAuthServerResponse.decoder, headers: authorization(options?.refreshJwt), params: {
'provider': provider.name,
'redirectURL': redirectUrl,
}, body: {
"loginOptions": options?.toMap(),
});
}

Expand All @@ -216,10 +227,12 @@ class DescopeClient extends HttpClient {

// SSO

Future<SsoServerResponse> ssoStart(String emailOrTenantId, String? redirectUrl) {
return post('auth/saml/authorize', SsoServerResponse.decoder, params: {
Future<SsoServerResponse> ssoStart(String emailOrTenantId, String? redirectUrl, SignInOptions? options) {
return post('auth/saml/authorize', SsoServerResponse.decoder, headers: authorization(options?.refreshJwt), params: {
'tenant': emailOrTenantId,
'redirectURL': redirectUrl,
}, body: {
"loginOptions": options?.toMap(),
});
}

Expand Down Expand Up @@ -264,12 +277,22 @@ class DescopeClient extends HttpClient {
'x-descope-sdk-version': '0.1.0',
};

Map<String, String> authorization(String value) {
return {'Authorization': 'Bearer ${config.projectId}:$value'};
Map<String, String> authorization(String? value) {
return value != null ? {'Authorization': 'Bearer ${config.projectId}:$value'} : {};
}
}

// Extensions
extension on SignInOptions {
String? get refreshJwt => stepupRefreshJwt ?? mfaRefreshJwt;

Map<String, dynamic> toMap() {
return {
"stepup": stepupRefreshJwt != null ? true : null,
"mfa": mfaRefreshJwt != null ? true : null,
"customClaims": customClaims.isNotEmpty ? customClaims : null,
};
}
}

extension on SignUpDetails {
Map<String, dynamic> toMap() {
Expand Down
8 changes: 4 additions & 4 deletions lib/src/internal/routes/enchanted_link.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ class EnchantedLink implements DescopeEnchantedLink {
}

@override
Future<EnchantedLinkResponse> signIn({required String loginId, String? uri}) async {
return (await client.enchantedLinkSignIn(loginId, uri)).convert();
Future<EnchantedLinkResponse> signIn({required String loginId, String? uri, SignInOptions? options}) async {
return (await client.enchantedLinkSignIn(loginId, uri, options)).convert();
}

@override
Future<EnchantedLinkResponse> signUpOrIn({required String loginId, String? uri}) async {
return (await client.enchantedLinkSignUpOrIn(loginId, uri)).convert();
Future<EnchantedLinkResponse> signUpOrIn({required String loginId, String? uri, SignInOptions? options}) async {
return (await client.enchantedLinkSignUpOrIn(loginId, uri, options)).convert();
}

@override
Expand Down
8 changes: 4 additions & 4 deletions lib/src/internal/routes/magic_link.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ class MagicLink implements DescopeMagicLink {
}

@override
Future<String> signIn({required DeliveryMethod method, required String loginId, String? uri}) async {
return (await client.magicLinkSignIn(method, loginId, uri)).convert(method);
Future<String> signIn({required DeliveryMethod method, required String loginId, String? uri, SignInOptions? options}) async {
return (await client.magicLinkSignIn(method, loginId, uri, options)).convert(method);
}

@override
Future<String> signUpOrIn({required DeliveryMethod method, required String loginId, String? uri}) async {
return (await client.magicLinkSignUpOrIn(method, loginId, uri)).convert(method);
Future<String> signUpOrIn({required DeliveryMethod method, required String loginId, String? uri, SignInOptions? options}) async {
return (await client.magicLinkSignUpOrIn(method, loginId, uri, options)).convert(method);
}

@override
Expand Down
4 changes: 2 additions & 2 deletions lib/src/internal/routes/oauth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class OAuth implements DescopeOAuth {
OAuth(this.client);

@override
Future<String> start({required OAuthProvider provider, String? redirectUrl}) async {
return (await client.oauthStart(provider, redirectUrl)).url;
Future<String> start({required OAuthProvider provider, String? redirectUrl, SignInOptions? options}) async {
return (await client.oauthStart(provider, redirectUrl, options)).url;
}

@override
Expand Down
8 changes: 4 additions & 4 deletions lib/src/internal/routes/otp.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ class Otp implements DescopeOtp {
}

@override
Future<String> signIn({required DeliveryMethod method, required String loginId}) async {
return (await client.otpSignIn(method, loginId)).convert(method);
Future<String> signIn({required DeliveryMethod method, required String loginId, SignInOptions? options}) async {
return (await client.otpSignIn(method, loginId, options)).convert(method);
}

@override
Future<String> signUpOrIn({required DeliveryMethod method, required String loginId}) async {
return (await client.otpSignUpIn(method, loginId)).convert(method);
Future<String> signUpOrIn({required DeliveryMethod method, required String loginId, SignInOptions? options}) async {
return (await client.otpSignUpIn(method, loginId, options)).convert(method);
}

@override
Expand Down
5 changes: 3 additions & 2 deletions lib/src/internal/routes/sso.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import '/src/sdk/routes.dart';
import '/src/types/responses.dart';
import '../../types/others.dart';
import '../http/descope_client.dart';
import 'shared.dart';

Expand All @@ -9,8 +10,8 @@ class Sso implements DescopeSso {
Sso(this.client);

@override
Future<String> start({required String emailOrTenantId, String? redirectUrl}) async {
return (await client.ssoStart(emailOrTenantId, redirectUrl)).url;
Future<String> start({required String emailOrTenantId, String? redirectUrl, SignInOptions? options}) async {
return (await client.ssoStart(emailOrTenantId, redirectUrl, options)).url;
}

@override
Expand Down
4 changes: 2 additions & 2 deletions lib/src/internal/routes/totp.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ class Totp implements DescopeTotp {
}

@override
Future<AuthenticationResponse> verify({required String loginId, required String code}) async {
return (await client.totpVerify(loginId, code)).convert();
Future<AuthenticationResponse> verify({required String loginId, required String code, SignInOptions? options}) async {
return (await client.totpVerify(loginId, code, options)).convert();
}
}

Expand Down
18 changes: 9 additions & 9 deletions lib/src/sdk/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ abstract class DescopeOtp {
///
/// The OTP code will be sent to the user identified by [loginId]
/// via a delivery [method] of choice.
Future<String> signIn({required DeliveryMethod method, required String loginId});
Future<String> signIn({required DeliveryMethod method, required String loginId, SignInOptions? options});

/// Authenticates an existing user if one exists, or creates a new user
/// using an OTP
Expand All @@ -85,7 +85,7 @@ abstract class DescopeOtp {
/// **Important**: Make sure the delivery information corresponding with
/// the delivery [method] is given either in the optional [user] parameter or as
/// the [loginId] itself, i.e., the email address, phone number, etc.
Future<String> signUpOrIn({required DeliveryMethod method, required String loginId});
Future<String> signUpOrIn({required DeliveryMethod method, required String loginId, SignInOptions? options});

/// Verifies an OTP [code] sent to the user.
///
Expand Down Expand Up @@ -140,7 +140,7 @@ abstract class DescopeTotp {
///
/// Returns an [AuthenticationResponse] if the provided [loginId] and the [code]
/// generated by an authenticator app match.
Future<AuthenticationResponse> verify({required String loginId, required String code});
Future<AuthenticationResponse> verify({required String loginId, required String code, SignInOptions? options});
}

/// Authenticate users using a password.
Expand Down Expand Up @@ -229,7 +229,7 @@ abstract class DescopeMagicLink {
///
/// **Important:** Make sure a default magic link URI is configured
/// in the Descope console, or provided by this call via [uri].
Future<String> signIn({required DeliveryMethod method, required String loginId, String? uri});
Future<String> signIn({required DeliveryMethod method, required String loginId, String? uri, SignInOptions? options});

/// Authenticates an existing user if one exists, or creates a new user
/// using a magic link.
Expand All @@ -243,7 +243,7 @@ abstract class DescopeMagicLink {
///
/// **Important:** Make sure a default magic link URI is configured
/// in the Descope console, or provided by this call via [uri].
Future<String> signUpOrIn({required DeliveryMethod method, required String loginId, String? uri});
Future<String> signUpOrIn({required DeliveryMethod method, required String loginId, String? uri, SignInOptions? options});

/// Updates an existing user by adding an [email] address.
///
Expand Down Expand Up @@ -312,7 +312,7 @@ abstract class DescopeEnchantedLink {
///
/// **Important:** Make sure a default Enchanted link URI is configured
/// in the Descope console, or provided via [uri] by this call.
Future<EnchantedLinkResponse> signIn({required String loginId, String? uri});
Future<EnchantedLinkResponse> signIn({required String loginId, String? uri, SignInOptions? options});

/// Authenticates an existing user if one exists, or create a new user using an
/// enchanted link, sent via email.
Expand All @@ -323,7 +323,7 @@ abstract class DescopeEnchantedLink {
///
/// **Important:** Make sure a default Enchanted link URI is configured
/// in the Descope console, or provided via [uri] by this call.
Future<EnchantedLinkResponse> signUpOrIn({required String loginId, String? uri});
Future<EnchantedLinkResponse> signUpOrIn({required String loginId, String? uri, SignInOptions? options});

/// Updates an existing user by adding an email address.
///
Expand Down Expand Up @@ -381,7 +381,7 @@ abstract class DescopeOAuth {
///
/// **Important:** Make sure a default OAuth redirect URL is configured
/// in the Descope console, or provided by this call via [redirectUrl].
Future<String> start({required OAuthProvider provider, String? redirectUrl});
Future<String> start({required OAuthProvider provider, String? redirectUrl, SignInOptions? options});

/// Completes an OAuth redirect chain.
///
Expand All @@ -406,7 +406,7 @@ abstract class DescopeSso {
///
/// **Important:** Make sure a SSO is set up correctly and a redirect URL is configured
/// in the Descope console, or provided by this call via [redirectUrl].
Future<String> start({required String emailOrTenantId, String? redirectUrl});
Future<String> start({required String emailOrTenantId, String? redirectUrl, SignInOptions? options});

/// Completes an SSO redirect chain.
///
Expand Down
2 changes: 1 addition & 1 deletion lib/src/session/token.dart
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ Map<String, dynamic> getTenants(Map<String, dynamic> claims) {
// JWT Decoding

Uint8List decodeEncodedFragment(String value) {
final length = value.length + 4 - value.length % 4;
final length = 4 * ((value.length + 3) / 4).floor();
final data = const Base64Decoder().convert(value.padRight(length, '='));
return data;
}
Expand Down
47 changes: 45 additions & 2 deletions lib/src/types/others.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ enum OAuthProvider {
apple,
}

// Classes

/// Used to provide additional details about a user in sign up calls.
class SignUpDetails {
final String? name;
Expand All @@ -26,3 +24,48 @@ class SignUpDetails {

SignUpDetails({this.name, this.email, this.phone});
}

/// Used to require additional behaviors when authenticating a user.
class SignInOptions {
/// Used to add layered security to your app by implementing Step-up authentication.
///
/// final session = Descope.sessionManager.session;
/// if (session == null) {
/// throw Exception('User is not logged in');
/// }
/// final options = SignInOptions(stepupRefreshJwt: session.refreshJwt);
/// final future = Descope.otp.signIn(method: DeliveryMethod.email, loginId: email, options: options);
///
/// After the Step-up authentication completes successfully the returned session JWT will
/// have an `su` claim with a value of `true`.
///
/// **Note:** The `su` claim is not set on the refresh JWT.
final String? stepupRefreshJwt;

/// Used to add layered security to your app by implementing Multi-factor authentication.
///
/// Assuming the user has already signed in successfully with one authentication method,
/// we can take the `refreshJwt` from the [AuthenticationResponse] and pass it as the
/// [mfaRefreshJwt] value to another authentication method.
///
/// final options = SignInOptions(mfaRefreshJwt: authResponse.refreshJwt);
/// final future = Descope.otp.signIn(method: DeliveryMethod.email, loginId: email, options: options);
///
/// After the MFA authentication completes successfully the `amr` claim in both the session
/// and refresh JWTs will be an array with an entry for each authentication method used.
final String? mfaRefreshJwt;

/// Adds additional custom claims to the user's JWT during authentication.
///
/// For example, the following code starts an OTP sign in and requests a custom claim
/// with the authenticated user's full name:
///
/// const options = SignInOptions(customClaims: {"name": "{{user.name}}"});
/// await Descope.otp.signIn(method: DeliveryMethod.email, loginId: email, options: options);
///
/// **Important:** Any custom claims added via this method are considered insecure and will
/// be nested under the `nsec` custom claim.
final Map<String, dynamic> customClaims;

const SignInOptions({this.stepupRefreshJwt, this.mfaRefreshJwt, this.customClaims = const {}});
}

0 comments on commit cb110f1

Please sign in to comment.