Skip to content

Commit

Permalink
feat: add firstScreen, directSignIn, loginHint, identifiers and extra…
Browse files Browse the repository at this point in the history
…Params sign-in parameters (#68)
  • Loading branch information
simeng-li authored Sep 6, 2024
1 parent 554a0d3 commit 10b7506
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 56 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
run: flutter pub get

- name: Analyze
run: flutter analyze --no-pub
run: flutter analyze --no-pub --no-fatal-infos

- name: Test
run: flutter test --no-pub --coverage
Expand All @@ -48,7 +48,7 @@ jobs:

- name: Analyze
working-directory: ./example
run: flutter analyze --no-pub
run: flutter analyze --no-pub --no-fatal-infos

- name: Test
working-directory: ./example
Expand Down
17 changes: 15 additions & 2 deletions lib/logto_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import '/src/modules/pkce.dart';
import '/src/modules/token_storage.dart';
import '/src/utilities/utils.dart' as utils;
import 'logto_core.dart' as logto_core;
import '/src/utilities/constants.dart';

export '/src/exceptions/logto_auth_exceptions.dart';
export '/src/interfaces/logto_interfaces.dart';
Expand Down Expand Up @@ -187,8 +188,15 @@ class LogtoClient {
}

// Sign in using the PKCE flow.
Future<void> signIn(String redirectUri,
[logto_core.InteractionMode? interactionMode]) async {
Future<void> signIn(
String redirectUri, {
logto_core.InteractionMode? interactionMode,
String? loginHint,
String? directSignIn,
FirstScreen? firstScreen,
List<IdentifierType>? identifiers,
Map<String, String>? extraParams,
}) async {
if (_loading) {
throw LogtoAuthException(
LogtoAuthExceptions.isLoadingError, 'Already signing in...');
Expand All @@ -212,6 +220,11 @@ class LogtoClient {
resources: config.resources,
scopes: config.scopes,
interactionMode: interactionMode,
loginHint: loginHint,
firstScreen: firstScreen,
directSignIn: directSignIn,
identifiers: identifiers,
extraParams: extraParams,
);

final redirectUriScheme = Uri.parse(redirectUri).scheme;
Expand Down
118 changes: 77 additions & 41 deletions lib/logto_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,12 @@ const String _requestContentType = 'application/x-www-form-urlencoded';

/**
* logto_core.dart
*
* This file is part of the Logto SDK.
*
* This file is part of the Logto SDK.
* It contains the core functionalities of the OIDC authentication flow.
* Use this module if you want to build your own custom SDK.
*/

/**
* By default Logto use sign-in as the landing page for the user.
* Use this enum to specify the interaction mode.
*
* - signIn: The user will be redirected to the sign-in page.
* - signUp: The user will be redirected to the sign-up page.
*/
enum InteractionMode { signIn, signUp }

extension InteractionModeExtension on InteractionMode {
String get value {
switch (this) {
case InteractionMode.signIn:
return 'signIn';
case InteractionMode.signUp:
return 'signUp';
default:
throw Exception("Invalid value");
}
}
}

/**
* Fetch the OIDC provider configuration.
*/
Expand Down Expand Up @@ -98,7 +76,6 @@ Future<LogtoRefreshTokenResponse> fetchTokenByRefreshToken({
required String refreshToken,
String? resource,
String? organizationId,
List<String>? scopes,
}) async {
Map<String, dynamic> payload = {
'grant_type': refreshTokenGrantType,
Expand All @@ -114,10 +91,6 @@ Future<LogtoRefreshTokenResponse> fetchTokenByRefreshToken({
payload.addAll({'organization_id': organizationId});
}

if (scopes != null && scopes.isNotEmpty) {
payload.addAll({'scope': scopes.join(' ')});
}

final response = await httpClient.post(Uri.parse(tokenEndPoint),
headers: {'Content-Type': _requestContentType}, body: payload);

Expand Down Expand Up @@ -158,16 +131,47 @@ Future<void> revoke({
* Generate the sign-in URI (Authorization URI).
* This URI will be used to initiate the OIDC authentication flow.
*/
Uri generateSignInUri(
{required String authorizationEndpoint,
required clientId,
required String redirectUri,
required String codeChallenge,
required String state,
List<String>? scopes,
List<String>? resources,
InteractionMode? interactionMode,
String prompt = _prompt}) {
Uri generateSignInUri({
required String authorizationEndpoint,
required clientId,
required String redirectUri,
required String codeChallenge,
required String state,
String prompt = _prompt,
List<String>? scopes,
List<String>? resources,
String? loginHint,
@Deprecated('Legacy parameter, use firstScreen instead')
InteractionMode? interactionMode,
/**
* Direct sign-in is a feature that allows you to skip the sign-in page,
* and directly sign in the user using a specific social or sso connector.
*
* The format should be `social:{connectorTarget}` or `sso:{connectorId}`.
*/
String? directSignIn,
/**
* The first screen to be shown in the sign-in experience.
*/
FirstScreen? firstScreen,
/**
* Identifier type of the first screen to be shown in the sign-in experience.
*
* This parameter is applicable only when the `firstScreen` is set to
* either `identifierSignIn` or `identifierRegister
*/
List<IdentifierType>? identifiers,
/**
* Extra parameters to be added to the sign-in URI.
*/
Map<String, String>? extraParams,
}) {
assert(
isValidDirectSignInFormat(directSignIn),
'Invalid format for directSignIn: $directSignIn, '
'expected one of `social:{connector}` or `sso:{connector}`',
);

var signInUri = Uri.parse(authorizationEndpoint);

Map<String, dynamic> queryParameters = {
Expand All @@ -194,16 +198,38 @@ Uri generateSignInUri(
queryParameters.addAll({'resource': resources});
}

if (loginHint != null) {
queryParameters.addAll({'login_hint': loginHint});
}

if (interactionMode != null) {
// need to align with the backend OIDC params name
queryParameters.addAll({'interaction_mode': interactionMode.value});
}

if (directSignIn != null) {
queryParameters.addAll({'direct_sign_in': directSignIn});
}

if (firstScreen != null) {
queryParameters.addAll({'first_screen': firstScreen.value});
}

if (identifiers != null && identifiers.isNotEmpty) {
queryParameters.addAll({
'identifier': identifiers.map((e) => e.value).join(' '),
});
}

if (extraParams != null) {
queryParameters.addAll(extraParams);
}

return addQueryParameters(signInUri, queryParameters);
}

/**
* Generate the sign-out URI (End Session URI).
* Generate the sign-out URI (End Session URI).
*/
Uri generateSignOutUri({
required String endSessionEndpoint,
Expand All @@ -220,7 +246,7 @@ Uri generateSignOutUri({

/**
* A utility function to verify and parse the code from the authorization callback URI.
*
*
* - verify the callback URI
* - verify the state
* - error detection
Expand Down Expand Up @@ -257,3 +283,13 @@ String verifyAndParseCodeFromCallbackUri(

return queryParams['code']!;
}

/**
* Verify the direct sign-in parameter format.
*/
bool isValidDirectSignInFormat(String? directSignIn) {
if (directSignIn == null) return true;

RegExp regex = RegExp(r'^(social|sso):[a-zA-Z0-9]+$');
return regex.hasMatch(directSignIn);
}
82 changes: 82 additions & 0 deletions lib/src/utilities/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,85 @@ String getOrganizationIdFromUrn(String organizationUrn) {

return organizationUrn.substring(organizationUrnPrefix.length);
}

/**
* @Deprecated use firstScreen instead
*
* By default Logto use sign-in as the landing page for the user.
* Use this enum to specify the interaction mode.
*
* - signIn: The user will be redirected to the sign-in page.
* - signUp: The user will be redirected to the sign-up page.
*/
enum InteractionMode { signIn, signUp }

extension InteractionModeExtension on InteractionMode {
String get value {
switch (this) {
case InteractionMode.signIn:
return 'signIn';
case InteractionMode.signUp:
return 'signUp';
default:
throw Exception("Invalid value");
}
}
}

/**
* The first screen to be shown in the sign-in experience.
*
* Note it's not a part of the OIDC standard, but a Logto-specific extension.
*/
enum FirstScreen {
signIn,
register,
identifierSignIn,
identifierRegister,
singleSignOn,
}

extension FirstScreenExtension on FirstScreen {
String get value {
switch (this) {
case FirstScreen.signIn:
return 'sign_in';
case FirstScreen.register:
return 'register';
case FirstScreen.identifierSignIn:
return 'identifier:sign_in';
case FirstScreen.identifierRegister:
return 'identifier:register';
case FirstScreen.singleSignOn:
return 'single_sign_on';
default:
throw Exception("Invalid value");
}
}
}

/**
* The type of the identifier supported by Logto.
* This field is used along with FirstScreen to specify the first screen to be shown in the sign-in experience.
* If specified, the first screen will be shown based on the identifier type.
*/
enum IdentifierType {
email,
phone,
username,
}

extension IdentifierTypeExtension on IdentifierType {
String get value {
switch (this) {
case IdentifierType.email:
return 'email';
case IdentifierType.phone:
return 'phone';
case IdentifierType.username:
return 'username';
default:
throw Exception("Invalid value");
}
}
}
Loading

0 comments on commit 10b7506

Please sign in to comment.