Skip to content

Commit 10b7506

Browse files
authored
feat: add firstScreen, directSignIn, loginHint, identifiers and extraParams sign-in parameters (#68)
1 parent 554a0d3 commit 10b7506

File tree

5 files changed

+306
-56
lines changed

5 files changed

+306
-56
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
run: flutter pub get
2828

2929
- name: Analyze
30-
run: flutter analyze --no-pub
30+
run: flutter analyze --no-pub --no-fatal-infos
3131

3232
- name: Test
3333
run: flutter test --no-pub --coverage
@@ -48,7 +48,7 @@ jobs:
4848

4949
- name: Analyze
5050
working-directory: ./example
51-
run: flutter analyze --no-pub
51+
run: flutter analyze --no-pub --no-fatal-infos
5252

5353
- name: Test
5454
working-directory: ./example

lib/logto_client.dart

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import '/src/modules/pkce.dart';
1010
import '/src/modules/token_storage.dart';
1111
import '/src/utilities/utils.dart' as utils;
1212
import 'logto_core.dart' as logto_core;
13+
import '/src/utilities/constants.dart';
1314

1415
export '/src/exceptions/logto_auth_exceptions.dart';
1516
export '/src/interfaces/logto_interfaces.dart';
@@ -187,8 +188,15 @@ class LogtoClient {
187188
}
188189

189190
// Sign in using the PKCE flow.
190-
Future<void> signIn(String redirectUri,
191-
[logto_core.InteractionMode? interactionMode]) async {
191+
Future<void> signIn(
192+
String redirectUri, {
193+
logto_core.InteractionMode? interactionMode,
194+
String? loginHint,
195+
String? directSignIn,
196+
FirstScreen? firstScreen,
197+
List<IdentifierType>? identifiers,
198+
Map<String, String>? extraParams,
199+
}) async {
192200
if (_loading) {
193201
throw LogtoAuthException(
194202
LogtoAuthExceptions.isLoadingError, 'Already signing in...');
@@ -212,6 +220,11 @@ class LogtoClient {
212220
resources: config.resources,
213221
scopes: config.scopes,
214222
interactionMode: interactionMode,
223+
loginHint: loginHint,
224+
firstScreen: firstScreen,
225+
directSignIn: directSignIn,
226+
identifiers: identifiers,
227+
extraParams: extraParams,
215228
);
216229

217230
final redirectUriScheme = Uri.parse(redirectUri).scheme;

lib/logto_core.dart

Lines changed: 77 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,12 @@ const String _requestContentType = 'application/x-www-form-urlencoded';
1717

1818
/**
1919
* logto_core.dart
20-
*
21-
* This file is part of the Logto SDK.
20+
*
21+
* This file is part of the Logto SDK.
2222
* It contains the core functionalities of the OIDC authentication flow.
2323
* Use this module if you want to build your own custom SDK.
2424
*/
2525

26-
/**
27-
* By default Logto use sign-in as the landing page for the user.
28-
* Use this enum to specify the interaction mode.
29-
*
30-
* - signIn: The user will be redirected to the sign-in page.
31-
* - signUp: The user will be redirected to the sign-up page.
32-
*/
33-
enum InteractionMode { signIn, signUp }
34-
35-
extension InteractionModeExtension on InteractionMode {
36-
String get value {
37-
switch (this) {
38-
case InteractionMode.signIn:
39-
return 'signIn';
40-
case InteractionMode.signUp:
41-
return 'signUp';
42-
default:
43-
throw Exception("Invalid value");
44-
}
45-
}
46-
}
47-
4826
/**
4927
* Fetch the OIDC provider configuration.
5028
*/
@@ -98,7 +76,6 @@ Future<LogtoRefreshTokenResponse> fetchTokenByRefreshToken({
9876
required String refreshToken,
9977
String? resource,
10078
String? organizationId,
101-
List<String>? scopes,
10279
}) async {
10380
Map<String, dynamic> payload = {
10481
'grant_type': refreshTokenGrantType,
@@ -114,10 +91,6 @@ Future<LogtoRefreshTokenResponse> fetchTokenByRefreshToken({
11491
payload.addAll({'organization_id': organizationId});
11592
}
11693

117-
if (scopes != null && scopes.isNotEmpty) {
118-
payload.addAll({'scope': scopes.join(' ')});
119-
}
120-
12194
final response = await httpClient.post(Uri.parse(tokenEndPoint),
12295
headers: {'Content-Type': _requestContentType}, body: payload);
12396

@@ -158,16 +131,47 @@ Future<void> revoke({
158131
* Generate the sign-in URI (Authorization URI).
159132
* This URI will be used to initiate the OIDC authentication flow.
160133
*/
161-
Uri generateSignInUri(
162-
{required String authorizationEndpoint,
163-
required clientId,
164-
required String redirectUri,
165-
required String codeChallenge,
166-
required String state,
167-
List<String>? scopes,
168-
List<String>? resources,
169-
InteractionMode? interactionMode,
170-
String prompt = _prompt}) {
134+
Uri generateSignInUri({
135+
required String authorizationEndpoint,
136+
required clientId,
137+
required String redirectUri,
138+
required String codeChallenge,
139+
required String state,
140+
String prompt = _prompt,
141+
List<String>? scopes,
142+
List<String>? resources,
143+
String? loginHint,
144+
@Deprecated('Legacy parameter, use firstScreen instead')
145+
InteractionMode? interactionMode,
146+
/**
147+
* Direct sign-in is a feature that allows you to skip the sign-in page,
148+
* and directly sign in the user using a specific social or sso connector.
149+
*
150+
* The format should be `social:{connectorTarget}` or `sso:{connectorId}`.
151+
*/
152+
String? directSignIn,
153+
/**
154+
* The first screen to be shown in the sign-in experience.
155+
*/
156+
FirstScreen? firstScreen,
157+
/**
158+
* Identifier type of the first screen to be shown in the sign-in experience.
159+
*
160+
* This parameter is applicable only when the `firstScreen` is set to
161+
* either `identifierSignIn` or `identifierRegister
162+
*/
163+
List<IdentifierType>? identifiers,
164+
/**
165+
* Extra parameters to be added to the sign-in URI.
166+
*/
167+
Map<String, String>? extraParams,
168+
}) {
169+
assert(
170+
isValidDirectSignInFormat(directSignIn),
171+
'Invalid format for directSignIn: $directSignIn, '
172+
'expected one of `social:{connector}` or `sso:{connector}`',
173+
);
174+
171175
var signInUri = Uri.parse(authorizationEndpoint);
172176

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

201+
if (loginHint != null) {
202+
queryParameters.addAll({'login_hint': loginHint});
203+
}
204+
197205
if (interactionMode != null) {
198206
// need to align with the backend OIDC params name
199207
queryParameters.addAll({'interaction_mode': interactionMode.value});
200208
}
201209

210+
if (directSignIn != null) {
211+
queryParameters.addAll({'direct_sign_in': directSignIn});
212+
}
213+
214+
if (firstScreen != null) {
215+
queryParameters.addAll({'first_screen': firstScreen.value});
216+
}
217+
218+
if (identifiers != null && identifiers.isNotEmpty) {
219+
queryParameters.addAll({
220+
'identifier': identifiers.map((e) => e.value).join(' '),
221+
});
222+
}
223+
224+
if (extraParams != null) {
225+
queryParameters.addAll(extraParams);
226+
}
227+
202228
return addQueryParameters(signInUri, queryParameters);
203229
}
204230

205231
/**
206-
* Generate the sign-out URI (End Session URI).
232+
* Generate the sign-out URI (End Session URI).
207233
*/
208234
Uri generateSignOutUri({
209235
required String endSessionEndpoint,
@@ -220,7 +246,7 @@ Uri generateSignOutUri({
220246

221247
/**
222248
* A utility function to verify and parse the code from the authorization callback URI.
223-
*
249+
*
224250
* - verify the callback URI
225251
* - verify the state
226252
* - error detection
@@ -257,3 +283,13 @@ String verifyAndParseCodeFromCallbackUri(
257283

258284
return queryParams['code']!;
259285
}
286+
287+
/**
288+
* Verify the direct sign-in parameter format.
289+
*/
290+
bool isValidDirectSignInFormat(String? directSignIn) {
291+
if (directSignIn == null) return true;
292+
293+
RegExp regex = RegExp(r'^(social|sso):[a-zA-Z0-9]+$');
294+
return regex.hasMatch(directSignIn);
295+
}

lib/src/utilities/constants.dart

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,85 @@ String getOrganizationIdFromUrn(String organizationUrn) {
2424

2525
return organizationUrn.substring(organizationUrnPrefix.length);
2626
}
27+
28+
/**
29+
* @Deprecated use firstScreen instead
30+
*
31+
* By default Logto use sign-in as the landing page for the user.
32+
* Use this enum to specify the interaction mode.
33+
*
34+
* - signIn: The user will be redirected to the sign-in page.
35+
* - signUp: The user will be redirected to the sign-up page.
36+
*/
37+
enum InteractionMode { signIn, signUp }
38+
39+
extension InteractionModeExtension on InteractionMode {
40+
String get value {
41+
switch (this) {
42+
case InteractionMode.signIn:
43+
return 'signIn';
44+
case InteractionMode.signUp:
45+
return 'signUp';
46+
default:
47+
throw Exception("Invalid value");
48+
}
49+
}
50+
}
51+
52+
/**
53+
* The first screen to be shown in the sign-in experience.
54+
*
55+
* Note it's not a part of the OIDC standard, but a Logto-specific extension.
56+
*/
57+
enum FirstScreen {
58+
signIn,
59+
register,
60+
identifierSignIn,
61+
identifierRegister,
62+
singleSignOn,
63+
}
64+
65+
extension FirstScreenExtension on FirstScreen {
66+
String get value {
67+
switch (this) {
68+
case FirstScreen.signIn:
69+
return 'sign_in';
70+
case FirstScreen.register:
71+
return 'register';
72+
case FirstScreen.identifierSignIn:
73+
return 'identifier:sign_in';
74+
case FirstScreen.identifierRegister:
75+
return 'identifier:register';
76+
case FirstScreen.singleSignOn:
77+
return 'single_sign_on';
78+
default:
79+
throw Exception("Invalid value");
80+
}
81+
}
82+
}
83+
84+
/**
85+
* The type of the identifier supported by Logto.
86+
* This field is used along with FirstScreen to specify the first screen to be shown in the sign-in experience.
87+
* If specified, the first screen will be shown based on the identifier type.
88+
*/
89+
enum IdentifierType {
90+
email,
91+
phone,
92+
username,
93+
}
94+
95+
extension IdentifierTypeExtension on IdentifierType {
96+
String get value {
97+
switch (this) {
98+
case IdentifierType.email:
99+
return 'email';
100+
case IdentifierType.phone:
101+
return 'phone';
102+
case IdentifierType.username:
103+
return 'username';
104+
default:
105+
throw Exception("Invalid value");
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)