Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions flutter_appauth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [End session](#end-session)
- [Handling errors](#handling-errors)
- [Ephemeral Sessions (iOS and macOS only)](#ephemeral-sessions-ios-and-macos-only)
- [Using a proxy redirect URL](#using-a-proxy-redirect-url)
- [Android setup](#android-setup)
- [iOS/macOS setup](#iosmacos-setup)
- [API docs](#api-docs)
Expand Down Expand Up @@ -165,6 +166,64 @@ With an ephemeral session there will be no warning like `"app_name" Wants to Use
The option `preferEphemeralSession = true` must only be used for the end session call if it is also used for the sign in call.
Otherwise, there will be still an active login session in the browser.

### Using a proxy redirect URL

Some identity providers only allow HTTPS redirect URIs and wil reject custom-scheme deep links (e.g. `com.example.myapp://oauth2redirect`) during the authorization request. A common solution is to host a small HTTPS endpoint that immediately redirects to your app's custom scheme. The `proxyRedirectUrl` parameter supports this pattern.

**How it works:**

1. Your app registers a custom-scheme redirect URL (e.g. `com.example.myapp://oauth2redirect`) with the OS so it can intercept the callback — this is `redirectUrl`.
2. You host an HTTPS endpoint (e.g. `https://myapp.example.com/oauth2redirect`) that simply redirects to the custom-scheme URL — this becomes `proxyRedirectUrl`.
3. When making the authorization request, `proxyRedirectUrl` is sent to the identity provider as the `redirect_uri` parameter so that it passes the provider's HTTPS validation.
4. The provider redirects the browser to `proxyRedirectUrl`, which in turn redirects to `redirectUrl`, and the OS hands the callback back to your app.
5. When exchanging the authorization code for tokens, `proxyRedirectUrl` must also be specified so that the `redirect_uri` in the token request matches what was used during authorization.

**Example:**

```dart
// Authorization + code exchange in one step
final AuthorizationTokenResponse result = await appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
'<client_id>',
'com.example.myapp://oauth2redirect', // redirectUrl: custom scheme the OS intercepts
proxyRedirectUrl: 'https://myapp.example.com/oauth2redirect', // sent to the provider as redirect_uri
discoveryUrl: '<discovery_url>',
scopes: ['openid', 'profile', 'email', 'offline_access'],
),
);
```

If you perform authorization and token exchange as separate steps, pass `proxyRedirectUrl` to both calls:

```dart
// Step 1: authorize
final AuthorizationResponse authResult = await appAuth.authorize(
AuthorizationRequest(
'<client_id>',
'com.example.myapp://oauth2redirect',
proxyRedirectUrl: 'https://myapp.example.com/oauth2redirect',
discoveryUrl: '<discovery_url>',
scopes: ['openid', 'profile', 'email', 'offline_access'],
),
);

// Step 2: exchange the code — proxyRedirectUrl must match what was sent during authorization
final TokenResponse tokenResult = await appAuth.token(
TokenRequest(
'<client_id>',
'com.example.myapp://oauth2redirect',
proxyRedirectUrl: 'https://myapp.example.com/oauth2redirect',
authorizationCode: authResult.authorizationCode,
discoveryUrl: '<discovery_url>',
codeVerifier: authResult.codeVerifier,
nonce: authResult.nonce,
scopes: ['openid', 'profile', 'email', 'offline_access'],
),
);
```

> **Note:** The `redirectUrl` still needs to be registered with the OS (via `Info.plist` on iOS/macOS and `build.gradle`/`AndroidManifest.xml` on Android) so the OS knows to route the deep link back to your app. Only `proxyRedirectUrl` is sent to the identity provider.

## Android setup

Go to the `build.gradle.kts` file for your Android app to specify the custom scheme so that there should be a section in it that look similar to the following but replace `<your_custom_scheme>` with the desired value:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ private AuthorizationTokenRequestParameters processAuthorizationTokenRequestArgu
final String issuer = (String) arguments.get("issuer");
final String discoveryUrl = (String) arguments.get("discoveryUrl");
final String redirectUrl = (String) arguments.get("redirectUrl");
final String proxyRedirectUrl = (String) arguments.get("proxyRedirectUrl");
final String loginHint = (String) arguments.get("loginHint");
final String nonce = (String) arguments.get("nonce");
clientSecret = (String) arguments.get("clientSecret");
Expand All @@ -228,6 +229,7 @@ private AuthorizationTokenRequestParameters processAuthorizationTokenRequestArgu
discoveryUrl,
scopes,
redirectUrl,
proxyRedirectUrl,
serviceConfigurationParameters,
additionalParameters,
loginHint,
Expand Down Expand Up @@ -266,12 +268,15 @@ private TokenRequestParameters processTokenRequestArguments(Map<String, Object>
final Map<String, String> additionalParameters =
(Map<String, String>) arguments.get("additionalParameters");
allowInsecureConnections = (boolean) arguments.get("allowInsecureConnections");
final String proxyRedirectUrl = (String) arguments.get("proxyRedirectUrl");
final String effectiveRedirectUrl = proxyRedirectUrl != null ? proxyRedirectUrl : redirectUrl;
return new TokenRequestParameters(
clientId,
issuer,
discoveryUrl,
scopes,
redirectUrl,
effectiveRedirectUrl,
null,
refreshToken,
authorizationCode,
codeVerifier,
Expand Down Expand Up @@ -317,6 +322,7 @@ private void handleAuthorizeMethodCall(
serviceConfiguration,
tokenRequestParameters.clientId,
tokenRequestParameters.redirectUrl,
tokenRequestParameters.proxyRedirectUrl,
tokenRequestParameters.scopes,
tokenRequestParameters.loginHint,
tokenRequestParameters.nonce,
Expand All @@ -336,6 +342,7 @@ public void onFetchConfigurationCompleted(
serviceConfiguration,
tokenRequestParameters.clientId,
tokenRequestParameters.redirectUrl,
tokenRequestParameters.proxyRedirectUrl,
tokenRequestParameters.scopes,
tokenRequestParameters.loginHint,
tokenRequestParameters.nonce,
Expand Down Expand Up @@ -409,16 +416,18 @@ private void performAuthorization(
AuthorizationServiceConfiguration serviceConfiguration,
String clientId,
String redirectUrl,
String proxyRedirectUrl,
ArrayList<String> scopes,
String loginHint,
String nonce,
Map<String, String> additionalParameters,
boolean exchangeCode,
ArrayList<String> promptValues,
String responseMode) {
String effectiveRedirectUrl = proxyRedirectUrl != null ? proxyRedirectUrl : redirectUrl;
AuthorizationRequest.Builder authRequestBuilder =
new AuthorizationRequest.Builder(
serviceConfiguration, clientId, ResponseTypeValues.CODE, Uri.parse(redirectUrl));
serviceConfiguration, clientId, ResponseTypeValues.CODE, Uri.parse(effectiveRedirectUrl));

if (scopes != null && !scopes.isEmpty()) {
authRequestBuilder.setScopes(scopes);
Expand Down Expand Up @@ -791,6 +800,7 @@ private class TokenRequestParameters {
final String discoveryUrl;
final ArrayList<String> scopes;
final String redirectUrl;
final String proxyRedirectUrl;
final String refreshToken;
final String grantType;
final String codeVerifier;
Expand All @@ -805,6 +815,7 @@ private TokenRequestParameters(
String discoveryUrl,
ArrayList<String> scopes,
String redirectUrl,
String proxyRedirectUrl,
String refreshToken,
String authorizationCode,
String codeVerifier,
Expand All @@ -817,6 +828,7 @@ private TokenRequestParameters(
this.discoveryUrl = discoveryUrl;
this.scopes = scopes;
this.redirectUrl = redirectUrl;
this.proxyRedirectUrl = proxyRedirectUrl;
this.refreshToken = refreshToken;
this.authorizationCode = authorizationCode;
this.codeVerifier = codeVerifier;
Expand Down Expand Up @@ -868,6 +880,7 @@ private AuthorizationTokenRequestParameters(
String discoveryUrl,
ArrayList<String> scopes,
String redirectUrl,
String proxyRedirectUrl,
Map<String, String> serviceConfigurationParameters,
Map<String, String> additionalParameters,
String loginHint,
Expand All @@ -880,6 +893,7 @@ private AuthorizationTokenRequestParameters(
discoveryUrl,
scopes,
redirectUrl,
proxyRedirectUrl,
null,
null,
null,
Expand Down
34 changes: 25 additions & 9 deletions flutter_appauth/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class _MyAppState extends State<MyApp> {
// For a list of client IDs, go to https://demo.duendesoftware.com
final String _clientId = 'interactive.public';
final String _redirectUrl = 'com.duendesoftware.demo:/oauthredirect';
final String? _proxyRedirectUrl =
null /*'https://my-server.com/auth/redirect?client=app'*/;
final String _issuer = 'https://demo.duendesoftware.com';
final String _discoveryUrl =
'https://demo.duendesoftware.com/.well-known/openid-configuration';
Expand Down Expand Up @@ -240,7 +242,11 @@ class _MyAppState extends State<MyApp> {
_setBusyState();
final TokenResponse result = await _appAuth.token(TokenRequest(
_clientId, _redirectUrl,
refreshToken: _refreshToken, issuer: _issuer, scopes: _scopes));
proxyRedirectUrl: _proxyRedirectUrl,
refreshToken: _refreshToken,
serviceConfiguration: _serviceConfiguration,
issuer: _issuer,
scopes: _scopes));
_processTokenResponse(result);
await _testApi(result);
} catch (e) {
Expand All @@ -255,6 +261,7 @@ class _MyAppState extends State<MyApp> {
_setBusyState();
final TokenResponse result = await _appAuth.token(TokenRequest(
_clientId, _redirectUrl,
proxyRedirectUrl: _proxyRedirectUrl,
authorizationCode: _authorizationCode,
discoveryUrl: _discoveryUrl,
codeVerifier: _codeVerifier,
Expand All @@ -272,9 +279,9 @@ class _MyAppState extends State<MyApp> {
Future<void> _signInWithNoCodeExchange() async {
try {
_setBusyState();
/*
/*
The discovery endpoint (_discoveryUrl) is used to find the
configuration. The code challenge generation can be checked here
configuration. The code challenge generation can be checked here
> https://github.com/MaikuB/flutter_appauth/search?q=challenge.
The code challenge is generated from the code verifier and only the
code verifier is included in the result. This because to get the token
Expand All @@ -285,15 +292,19 @@ class _MyAppState extends State<MyApp> {
*/
final AuthorizationResponse result = await _appAuth.authorize(
AuthorizationRequest(_clientId, _redirectUrl,
discoveryUrl: _discoveryUrl, scopes: _scopes, loginHint: 'bob'),
proxyRedirectUrl: _proxyRedirectUrl,
discoveryUrl: _discoveryUrl,
scopes: _scopes,
loginHint: 'bob'),
);

/*
/*
or just use the issuer
var result = await _appAuth.authorize(
AuthorizationRequest(
_clientId,
_redirectUrl,
proxyRedirectUrl: _proxyRedirectUrl,
issuer: _issuer,
scopes: _scopes,
),
Expand All @@ -317,6 +328,7 @@ class _MyAppState extends State<MyApp> {
// use the discovery endpoint to find the configuration
final AuthorizationResponse result = await _appAuth.authorize(
AuthorizationRequest(_clientId, _redirectUrl,
proxyRedirectUrl: _proxyRedirectUrl,
discoveryUrl: _discoveryUrl,
scopes: _scopes,
loginHint: 'bob',
Expand Down Expand Up @@ -344,6 +356,7 @@ class _MyAppState extends State<MyApp> {
final AuthorizationTokenResponse result =
await _appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(_clientId, _redirectUrl,
proxyRedirectUrl: _proxyRedirectUrl,
serviceConfiguration: _serviceConfiguration,
scopes: _scopes,
externalUserAgent: externalUserAgent),
Expand All @@ -359,6 +372,7 @@ class _MyAppState extends State<MyApp> {
final AuthorizationTokenResponse result = await _appAuth
.authorizeAndExchangeCode(
AuthorizationTokenRequest(_clientId, _redirectUrl,
proxyRedirectUrl: _proxyRedirectUrl,
serviceConfiguration: _serviceConfiguration,
scopes: _scopes,
promptValues: ['login']),
Expand Down Expand Up @@ -411,11 +425,13 @@ class _MyAppState extends State<MyApp> {

void _processAuthTokenResponse(AuthorizationTokenResponse response) {
setState(() {
_accessToken = _accessTokenTextController.text = response.accessToken!;
_idToken = _idTokenTextController.text = response.idToken!;
_refreshToken = _refreshTokenTextController.text = response.refreshToken!;
_accessToken =
_accessTokenTextController.text = response.accessToken ?? '';
_idToken = _idTokenTextController.text = response.idToken ?? '';
_refreshToken =
_refreshTokenTextController.text = response.refreshToken ?? '';
_accessTokenExpirationTextController.text =
response.accessTokenExpirationDateTime!.toIso8601String();
response.accessTokenExpirationDateTime?.toIso8601String() ?? '';
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#import "AppAuthIOSAuthorization.h"
#import "FlutterAppAuthProxyUserAgent.h"

@implementation AppAuthIOSAuthorization

Expand All @@ -8,6 +9,7 @@ @implementation AppAuthIOSAuthorization
clientSecret:(NSString *)clientSecret
scopes:(NSArray *)scopes
redirectUrl:(NSString *)redirectUrl
proxyRedirectUrl:(NSString *)proxyRedirectUrl
additionalParameters:(NSDictionary *)additionalParameters
externalUserAgent:(NSNumber *)externalUserAgent
result:(FlutterResult)result
Expand All @@ -16,13 +18,14 @@ @implementation AppAuthIOSAuthorization
NSString *codeVerifier = [OIDAuthorizationRequest generateCodeVerifier];
NSString *codeChallenge =
[OIDAuthorizationRequest codeChallengeS256ForVerifier:codeVerifier];
NSString *effectiveRedirectUrl = proxyRedirectUrl ?: redirectUrl;

OIDAuthorizationRequest *request = [[OIDAuthorizationRequest alloc]
initWithConfiguration:serviceConfiguration
clientId:clientId
clientSecret:clientSecret
scope:[OIDScopeUtilities scopesWithArray:scopes]
redirectURL:[NSURL URLWithString:redirectUrl]
redirectURL:[NSURL URLWithString:effectiveRedirectUrl]
responseType:OIDResponseTypeCode
state:[OIDAuthorizationRequest generateState]
nonce:nonce != nil
Expand All @@ -36,7 +39,9 @@ @implementation AppAuthIOSAuthorization
if (exchangeCode) {
id<OIDExternalUserAgent> agent =
[self userAgentWithViewController:rootViewController
externalUserAgent:externalUserAgent];
externalUserAgent:externalUserAgent
redirectUrl:redirectUrl
proxyRedirectUrl:proxyRedirectUrl];
return [OIDAuthState
authStateByPresentingAuthorizationRequest:request
externalUserAgent:agent
Expand Down Expand Up @@ -68,7 +73,9 @@ @implementation AppAuthIOSAuthorization
} else {
id<OIDExternalUserAgent> agent =
[self userAgentWithViewController:rootViewController
externalUserAgent:externalUserAgent];
externalUserAgent:externalUserAgent
redirectUrl:redirectUrl
proxyRedirectUrl:proxyRedirectUrl];
return [OIDAuthorizationService
presentAuthorizationRequest:request
externalUserAgent:agent
Expand Down Expand Up @@ -136,7 +143,9 @@ @implementation AppAuthIOSAuthorization
UIViewController *rootViewController = [self rootViewController];
id<OIDExternalUserAgent> externalUserAgent =
[self userAgentWithViewController:rootViewController
externalUserAgent:requestParameters.externalUserAgent];
externalUserAgent:requestParameters.externalUserAgent
redirectUrl:@""
proxyRedirectUrl:nil];

return [OIDAuthorizationService
presentEndSessionRequest:endSessionRequest
Expand Down Expand Up @@ -164,12 +173,25 @@ @implementation AppAuthIOSAuthorization

- (id<OIDExternalUserAgent>)
userAgentWithViewController:(UIViewController *)rootViewController
externalUserAgent:(NSNumber *)externalUserAgent {
if ([externalUserAgent integerValue] == EphemeralASWebAuthenticationSession) {
externalUserAgent:(NSNumber *)externalUserAgent
redirectUrl:(NSString *)redirectUrl
proxyRedirectUrl:(NSString *_Nullable)proxyRedirectUrl {
NSInteger agentValue = [externalUserAgent integerValue];
if (proxyRedirectUrl &&
(agentValue == ASWebAuthenticationSession ||
agentValue == EphemeralASWebAuthenticationSession)) {
NSString *callbackScheme = [NSURL URLWithString:redirectUrl].scheme;
return [[FlutterAppAuthProxyUserAgent alloc]
initWithPresentingViewController:rootViewController
callbackScheme:callbackScheme
proxyRedirectUrl:proxyRedirectUrl
ephemeral:(agentValue == EphemeralASWebAuthenticationSession)];
}
if (agentValue == EphemeralASWebAuthenticationSession) {
return [[OIDExternalUserAgentIOSNoSSO alloc]
initWithPresentingViewController:rootViewController];
}
if ([externalUserAgent integerValue] == SafariViewController) {
if (agentValue == SafariViewController) {
return [[OIDExternalUserAgentIOSSafariViewController alloc]
initWithPresentingViewController:rootViewController];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ typedef NS_ENUM(NSInteger, ExternalUserAgent) {
clientSecret:(NSString *)clientSecret
scopes:(NSArray *)scopes
redirectUrl:(NSString *)redirectUrl
proxyRedirectUrl:(NSString *_Nullable)proxyRedirectUrl
additionalParameters:(NSDictionary *)additionalParameters
externalUserAgent:(NSNumber *)externalUserAgent
result:(FlutterResult)result
Expand Down
Loading