diff --git a/example/pubspec.lock b/example/pubspec.lock index 7f90aa7..b0d81cb 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" crypto: dependency: transitive description: @@ -135,14 +135,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" lints: dependency: transitive description: @@ -155,18 +147,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -296,10 +288,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: @@ -336,10 +328,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" typed_data: dependency: transitive description: @@ -356,6 +348,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" win32: dependency: transitive description: @@ -373,5 +373,5 @@ packages: source: hosted version: "1.0.0" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.3.0" diff --git a/lib/flutter_authgear.dart b/lib/flutter_authgear.dart index 97d8f61..8b01fe3 100644 --- a/lib/flutter_authgear.dart +++ b/lib/flutter_authgear.dart @@ -32,6 +32,7 @@ export 'src/type.dart' export 'src/storage.dart' show TokenStorage, TransientTokenStorage, PersistentTokenStorage; export 'src/container.dart' show SessionStateChangeEvent, Authgear; +export 'src/experimental.dart' show AuthgearExperimental, AuthenticateRequest; export 'src/exception.dart' show AuthgearException, diff --git a/lib/src/container.dart b/lib/src/container.dart index 5c5cec5..121ae13 100644 --- a/lib/src/container.dart +++ b/lib/src/container.dart @@ -10,6 +10,7 @@ import 'code_verifier.dart'; import 'exception.dart'; import 'base64.dart'; import 'id_token.dart'; +import 'experimental.dart'; import 'native.dart' as native; class SessionStateChangeEvent { @@ -31,6 +32,102 @@ Future _getXDeviceInfo() async { return xDeviceInfo; } +class InternalAuthenticateRequest { + final Uri url; + final String redirectURI; + final CodeVerifier verifier; + + InternalAuthenticateRequest( + {required this.url, required this.redirectURI, required this.verifier}); +} + +class AuthenticateOptions { + final String redirectURI; + final bool isSsoEnabled; + final String? state; + final List? prompt; + final String? loginHint; + final List? uiLocales; + final ColorScheme? colorScheme; + final String? wechatRedirectURI; + final AuthenticationPage? page; + + AuthenticateOptions({ + required this.redirectURI, + required this.isSsoEnabled, + this.state, + this.prompt, + this.loginHint, + this.uiLocales, + this.colorScheme, + this.wechatRedirectURI, + this.page, + }); + + OIDCAuthenticationRequest toRequest(String clientID, CodeVerifier verifier) { + return OIDCAuthenticationRequest( + clientID: clientID, + redirectURI: redirectURI, + responseType: "code", + scope: [ + "openid", + "offline_access", + "https://authgear.com/scopes/full-access", + ], + isSsoEnabled: isSsoEnabled, + codeChallenge: verifier.codeChallenge, + prompt: prompt, + uiLocales: uiLocales, + colorScheme: colorScheme, + page: page, + state: state, + loginHint: loginHint, + wechatRedirectURI: wechatRedirectURI, + ); + } +} + +class ReauthenticateOptions { + final String redirectURI; + final bool isSsoEnabled; + final String? state; + final List? uiLocales; + final ColorScheme? colorScheme; + final String? wechatRedirectURI; + final int? maxAge; + + ReauthenticateOptions({ + required this.redirectURI, + required this.isSsoEnabled, + this.state, + this.uiLocales, + this.colorScheme, + this.wechatRedirectURI, + this.maxAge, + }); + + OIDCAuthenticationRequest toRequest( + String clientID, String idTokenHint, CodeVerifier verifier) { + final oidcRequest = OIDCAuthenticationRequest( + clientID: clientID, + redirectURI: redirectURI, + responseType: "code", + scope: [ + "openid", + "https://authgear.com/scopes/full-access", + ], + isSsoEnabled: isSsoEnabled, + codeChallenge: verifier.codeChallenge, + uiLocales: uiLocales, + colorScheme: colorScheme, + idTokenHint: idTokenHint, + maxAge: maxAge, + wechatRedirectURI: wechatRedirectURI, + ); + return oidcRequest; + } +} + // It seems that dart's convention of iOS' delegate is individual property of write-only function // See https://api.dart.dev/stable/2.16.2/dart-io/HttpClient/authenticate.html class Authgear implements AuthgearHttpClientDelegate { @@ -43,6 +140,7 @@ class Authgear implements AuthgearHttpClientDelegate { final TokenStorage _tokenStorage; final ContainerStorage _storage; late final APIClient _apiClient; + late final AuthgearExperimental experimental; SessionState _sessionStateRaw = SessionState.unknown; SessionState get sessionState => _sessionStateRaw; @@ -99,6 +197,7 @@ class Authgear implements AuthgearHttpClientDelegate { plainHttpClient: plainHttpClient, authgearHttpClient: authgearHttpClient, ); + experimental = AuthgearExperimental(this); } Future configure() async { @@ -115,48 +214,72 @@ class Authgear implements AuthgearHttpClientDelegate { .add(SessionStateChangeEvent(instance: this, reason: r)); } + Future internalBuildAuthorizationURL( + OIDCAuthenticationRequest oidcRequest) async { + final config = await _apiClient.fetchOIDCConfiguration(); + final authenticationURL = Uri.parse(config.authorizationEndpoint) + .replace(queryParameters: oidcRequest.toQueryParameters()); + return authenticationURL; + } + + Future internalCreateAuthenticateRequest( + AuthenticateOptions options) async { + final codeVerifier = CodeVerifier(_rng); + final oidcRequest = options.toRequest(clientID, codeVerifier); + final url = await internalBuildAuthorizationURL(oidcRequest); + + return InternalAuthenticateRequest( + url: url, + redirectURI: oidcRequest.redirectURI, + verifier: codeVerifier, + ); + } + Future authenticate({ required String redirectURI, List? prompt, List? uiLocales, ColorScheme? colorScheme, AuthenticationPage? page, + String? state, String? wechatRedirectURI, }) async { - final codeVerifier = CodeVerifier(_rng); - final oidcRequest = OIDCAuthenticationRequest( - clientID: clientID, + final authRequest = + await internalCreateAuthenticateRequest(AuthenticateOptions( redirectURI: redirectURI, - responseType: "code", - scope: [ - "openid", - "offline_access", - "https://authgear.com/scopes/full-access", - ], isSsoEnabled: isSsoEnabled, - codeChallenge: codeVerifier.codeChallenge, + state: state, prompt: prompt, uiLocales: uiLocales, colorScheme: colorScheme, - page: page, wechatRedirectURI: wechatRedirectURI, - ); - final config = await _apiClient.fetchOIDCConfiguration(); - final authenticationURL = Uri.parse(config.authorizationEndpoint) - .replace(queryParameters: oidcRequest.toQueryParameters()); + page: page, + )); final resultURL = await native.authenticate( - url: authenticationURL.toString(), - redirectURI: redirectURI, + url: authRequest.url.toString(), + redirectURI: authRequest.redirectURI, preferEphemeral: !isSsoEnabled, wechatRedirectURI: wechatRedirectURI, onWechatRedirectURI: _onWechatRedirectURI, ); - final xDeviceInfo = await _getXDeviceInfo(); - return await _finishAuthentication( + return await internalFinishAuthentication( url: Uri.parse(resultURL), redirectURI: redirectURI, - codeVerifier: codeVerifier, - xDeviceInfo: xDeviceInfo); + codeVerifier: authRequest.verifier); + } + + Future internalCreateReauthenticateRequest( + String idTokenHint, + ReauthenticateOptions options, + ) async { + final codeVerifier = CodeVerifier(_rng); + final oidcRequest = options.toRequest(clientID, idTokenHint, codeVerifier); + final url = await internalBuildAuthorizationURL(oidcRequest); + return InternalAuthenticateRequest( + url: url, + redirectURI: oidcRequest.redirectURI, + verifier: codeVerifier, + ); } Future reauthenticate({ @@ -176,28 +299,26 @@ class Authgear implements AuthgearHttpClientDelegate { ); } - final codeVerifier = CodeVerifier(_rng); - final oidcRequest = OIDCAuthenticationRequest( - clientID: clientID, + final idTokenHint = this.idTokenHint; + + if (idTokenHint == null) { + throw Exception("authenticated user required"); + } + + final options = ReauthenticateOptions( redirectURI: redirectURI, - responseType: "code", - scope: [ - "openid", - "https://authgear.com/scopes/full-access", - ], isSsoEnabled: isSsoEnabled, - codeChallenge: codeVerifier.codeChallenge, uiLocales: uiLocales, colorScheme: colorScheme, - idTokenHint: idTokenHint, - maxAge: maxAge, wechatRedirectURI: wechatRedirectURI, + maxAge: maxAge, ); - final config = await _apiClient.fetchOIDCConfiguration(); - final authenticationURL = Uri.parse(config.authorizationEndpoint) - .replace(queryParameters: oidcRequest.toQueryParameters()); + + final request = + await internalCreateReauthenticateRequest(idTokenHint, options); + final resultURL = await native.authenticate( - url: authenticationURL.toString(), + url: request.url.toString(), redirectURI: redirectURI, preferEphemeral: !isSsoEnabled, wechatRedirectURI: wechatRedirectURI, @@ -207,7 +328,7 @@ class Authgear implements AuthgearHttpClientDelegate { return await _finishReauthentication( url: Uri.parse(resultURL), redirectURI: redirectURI, - codeVerifier: codeVerifier, + codeVerifier: request.verifier, xDeviceInfo: xDeviceInfo); } @@ -215,15 +336,16 @@ class Authgear implements AuthgearHttpClientDelegate { return _getUserInfo(); } - Future openURL({ - required String url, + Future internalGenerateURL({ + required String redirectURI, + List? uiLocales, + ColorScheme? colorScheme, String? wechatRedirectURI, }) async { final refreshToken = _refreshToken; if (refreshToken == null) { - throw Exception("openURL requires authenticated user"); + throw Exception("authenticated user required"); } - final appSessionTokenResp = await _getAppSessionToken(refreshToken); final loginHint = Uri.parse("https://authgear.com/login_hint").replace(queryParameters: { @@ -233,7 +355,7 @@ class Authgear implements AuthgearHttpClientDelegate { final oidcRequest = OIDCAuthenticationRequest( clientID: clientID, - redirectURI: url, + redirectURI: redirectURI, responseType: "none", scope: [ "openid", @@ -243,11 +365,28 @@ class Authgear implements AuthgearHttpClientDelegate { isSsoEnabled: isSsoEnabled, prompt: [PromptOption.none], loginHint: loginHint, + uiLocales: uiLocales, + colorScheme: colorScheme, wechatRedirectURI: wechatRedirectURI, ); final config = await _apiClient.fetchOIDCConfiguration(); - final targetURL = Uri.parse(config.authorizationEndpoint) + return Uri.parse(config.authorizationEndpoint) .replace(queryParameters: oidcRequest.toQueryParameters()); + } + + Future openURL({ + required String url, + String? wechatRedirectURI, + }) async { + final refreshToken = _refreshToken; + if (refreshToken == null) { + throw Exception("openURL requires authenticated user"); + } + + final targetURL = await internalGenerateURL( + redirectURI: url, + wechatRedirectURI: wechatRedirectURI, + ); await native.openURL( url: targetURL.toString(), wechatRedirectURI: wechatRedirectURI, @@ -535,12 +674,10 @@ class Authgear implements AuthgearHttpClientDelegate { onWechatRedirectURI: _onWechatRedirectURI, preferEphemeral: !isSsoEnabled, ); - final xDeviceInfo = await _getXDeviceInfo(); - final userInfo = await _finishAuthentication( + final userInfo = await internalFinishAuthentication( url: Uri.parse(resultURL), redirectURI: redirectURI, - codeVerifier: codeVerifier, - xDeviceInfo: xDeviceInfo); + codeVerifier: codeVerifier); await _disableAnonymous(); await disableBiometric(); return userInfo; @@ -675,12 +812,12 @@ class Authgear implements AuthgearHttpClientDelegate { return tokenResponse; } - Future _finishAuthentication({ + Future internalFinishAuthentication({ required Uri url, required String redirectURI, required CodeVerifier codeVerifier, - required String xDeviceInfo, }) async { + final xDeviceInfo = await _getXDeviceInfo(); final tokenResponse = await _exchangeCode( url: url, redirectURI: redirectURI, diff --git a/lib/src/experimental.dart b/lib/src/experimental.dart new file mode 100644 index 0000000..59a9f1f --- /dev/null +++ b/lib/src/experimental.dart @@ -0,0 +1,93 @@ +import 'code_verifier.dart'; +import 'container.dart'; +import 'type.dart'; + +class AuthgearExperimental { + Authgear authgear; + + AuthgearExperimental(this.authgear); + + Future generateURL({ + required String redirectURI, + }) async { + final uri = await authgear.internalGenerateURL( + redirectURI: redirectURI, + ); + return uri; + } + + Future createAuthenticateRequest({ + required String redirectURI, + String? state, + List? prompt, + String? loginHint, + List? uiLocales, + ColorScheme? colorScheme, + String? wechatRedirectURI, + AuthenticationPage? page, + }) async { + final options = AuthenticateOptions( + redirectURI: redirectURI, + isSsoEnabled: authgear.isSsoEnabled, + state: state, + prompt: prompt, + loginHint: loginHint, + uiLocales: uiLocales, + colorScheme: colorScheme, + wechatRedirectURI: wechatRedirectURI, + page: page, + ); + final request = await authgear.internalCreateAuthenticateRequest(options); + return AuthenticateRequest.fromInternal(request); + } + + Future createReauthenticateRequest({ + required String redirectURI, + required String idTokenHint, + String? state, + List? uiLocales, + ColorScheme? colorScheme, + String? wechatRedirectURI, + }) async { + final options = ReauthenticateOptions( + redirectURI: redirectURI, + isSsoEnabled: authgear.isSsoEnabled, + state: state, + uiLocales: uiLocales, + colorScheme: colorScheme, + wechatRedirectURI: wechatRedirectURI, + ); + final request = await authgear.internalCreateReauthenticateRequest( + idTokenHint, options); + return AuthenticateRequest.fromInternal(request); + } + + Future finishAuthentication({ + required Uri url, + required AuthenticateRequest request, + }) async { + final userInfo = await authgear.internalFinishAuthentication( + url: url, + redirectURI: request.redirectURI, + codeVerifier: request._verifier, + ); + return userInfo; + } +} + +class AuthenticateRequest { + final Uri url; + final String redirectURI; + final CodeVerifier _verifier; + + AuthenticateRequest({ + required this.url, + required this.redirectURI, + required CodeVerifier verifier, + }) : _verifier = verifier; + + AuthenticateRequest.fromInternal(InternalAuthenticateRequest internalRequest) + : url = internalRequest.url, + redirectURI = internalRequest.redirectURI, + _verifier = internalRequest.verifier; +}