From e20b269e124a9019d06baba439fe7acb6ea2e74a Mon Sep 17 00:00:00 2001 From: Adam Michel Date: Thu, 31 Jan 2019 14:57:04 -0800 Subject: [PATCH 1/4] Add option to disable PKCE for non-compliant providers like Dropbox. --- index.d.ts | 1 + index.js | 2 ++ ios/RNAppAuth.m | 12 ++++++++---- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index 7ca7d25a..355ce50d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -30,6 +30,7 @@ export type AuthConfiguration = BaseAuthConfiguration & { additionalParameters?: BuiltInParameters & { [name: string]: string }; dangerouslyAllowInsecureHttpRequests?: boolean; useNonce?: boolean; + usePKCE?: boolean; }; export interface AuthorizeResult { diff --git a/index.js b/index.js index 3318414f..5012a2bf 100644 --- a/index.js +++ b/index.js @@ -29,6 +29,7 @@ export const authorize = ({ clientSecret, scopes, useNonce = true, + usePKCE = true, additionalParameters, serviceConfiguration, dangerouslyAllowInsecureHttpRequests = false, @@ -54,6 +55,7 @@ export const authorize = ({ if (Platform.OS === 'ios') { nativeMethodArguments.push(useNonce); + nativeMethodArguments.push(usePKCE); } return RNAppAuth.authorize(...nativeMethodArguments); diff --git a/ios/RNAppAuth.m b/ios/RNAppAuth.m index 3386ea4b..10f4c393 100644 --- a/ios/RNAppAuth.m +++ b/ios/RNAppAuth.m @@ -39,6 +39,7 @@ - (dispatch_queue_t)methodQueue additionalParameters: (NSDictionary *_Nullable) additionalParameters serviceConfiguration: (NSDictionary *_Nullable) serviceConfiguration useNonce: (BOOL *) useNonce + usePKCE: (BOOL *) usePKCE resolve: (RCTPromiseResolveBlock) resolve reject: (RCTPromiseRejectBlock) reject) { @@ -51,6 +52,7 @@ - (dispatch_queue_t)methodQueue clientSecret: clientSecret scopes: scopes useNonce: useNonce + usePKCE: usePKCE additionalParameters: additionalParameters resolve: resolve reject: reject]; @@ -67,6 +69,7 @@ - (dispatch_queue_t)methodQueue clientSecret: clientSecret scopes: scopes useNonce: useNonce + usePKCE: usePKCE additionalParameters: additionalParameters resolve: resolve reject: reject]; @@ -165,13 +168,14 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration clientSecret: (NSString *) clientSecret scopes: (NSArray *) scopes useNonce: (BOOL *) useNonce + usePKCE: (BOOL *) usePKCE additionalParameters: (NSDictionary *_Nullable) additionalParameters resolve: (RCTPromiseResolveBlock) resolve reject: (RCTPromiseRejectBlock) reject { - NSString *codeVerifier = [[self class] generateCodeVerifier]; - NSString *codeChallenge = [[self class] codeChallengeS256ForVerifier:codeVerifier]; + NSString *codeVerifier = usePKCE ? [[self class] generateCodeVerifier] : nil; + NSString *codeChallenge = usePKCE ? [[self class] codeChallengeS256ForVerifier:codeVerifier] : nil; NSString *nonce = useNonce ? [[self class] generateState] : nil; // builds authentication request @@ -186,7 +190,7 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration nonce:nonce codeVerifier:codeVerifier codeChallenge:codeChallenge - codeChallengeMethod:OIDOAuthorizationRequestCodeChallengeMethodS256 + codeChallengeMethod: usePKCE ? OIDOAuthorizationRequestCodeChallengeMethodS256 : nil additionalParameters:additionalParameters]; // performs authentication request @@ -205,7 +209,7 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration strongSelf->_currentSession = nil; if (authState) { resolve([self formatResponse:authState.lastTokenResponse - withAdditionalParameters:authState.lastAuthorizationResponse.additionalParameters]); + withAdditionalParameters:authState.lastTokenResponse.additionalParameters]); } else { reject(@"RNAppAuth Error", [error localizedDescription], error); } From d71ac4d0356618f288b38ecfac0aae58fc9dc719 Mon Sep 17 00:00:00 2001 From: Adam Michel Date: Thu, 31 Jan 2019 15:27:50 -0800 Subject: [PATCH 2/4] Update README.md and tests. --- README.md | 1 + index.spec.js | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index baa83201..3e2e3ba6 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ with optional overrides. `hello=world&foo=bar` to the authorization request. * **dangerouslyAllowInsecureHttpRequests** - (`boolean`) _ANDROID_ whether to allow requests over plain HTTP or with self-signed SSL certificates. :warning: Can be useful for testing against local server, _should not be used in production._ This setting has no effect on iOS; to enable insecure HTTP requests, add a [NSExceptionAllowsInsecureHTTPLoads exception](https://cocoacasts.com/how-to-add-app-transport-security-exception-domains) to your App Transport Security settings. * **useNonce** - (`boolean`) _IOS_ (default: true) optionally allows not sending the nonce parameter, to support non-compliant providers +* **usePKCE** - (`boolean`) _IOS_ (default: true) optionally allows not sending the code_challenge parameter and skipping PKCE code verification, to support non-compliant providers. #### result diff --git a/index.spec.js b/index.spec.js index 83cdffcd..31b4ec63 100644 --- a/index.spec.js +++ b/index.spec.js @@ -33,6 +33,7 @@ describe('AppAuth', () => { serviceConfiguration: null, scopes: ['my-scope'], useNonce: true, + usePKCE: true, }; describe('authorize', () => { @@ -89,7 +90,8 @@ describe('AppAuth', () => { config.scopes, config.additionalParameters, config.serviceConfiguration, - config.useNonce + config.useNonce, + config.usePKCE ); }); @@ -286,6 +288,7 @@ describe('AppAuth', () => { config.scopes, config.additionalParameters, config.serviceConfiguration, + true, true ); }); @@ -300,6 +303,43 @@ describe('AppAuth', () => { config.scopes, config.additionalParameters, config.serviceConfiguration, + false, + true + ); + }); + }); + + describe('iOS-specific usePKCE parameter', () => { + beforeEach(() => { + require('react-native').Platform.OS = 'ios'; + }); + + it('calls the native wrapper with default value `true`', () => { + authorize(config, { refreshToken: 'such-token' }); + expect(mockAuthorize).toHaveBeenCalledWith( + config.issuer, + config.redirectUrl, + config.clientId, + config.clientSecret, + config.scopes, + config.additionalParameters, + config.serviceConfiguration, + config.useNonce, + true + ); + }); + + it('calls the native wrapper with passed value `false`', () => { + authorize({ ...config, usePKCE: false }, { refreshToken: 'such-token' }); + expect(mockAuthorize).toHaveBeenCalledWith( + config.issuer, + config.redirectUrl, + config.clientId, + config.clientSecret, + config.scopes, + config.additionalParameters, + config.serviceConfiguration, + config.useNonce, false ); }); From 1a0ea63c314f49714a309ec0625f0b9d84a30771 Mon Sep 17 00:00:00 2001 From: Adam Michel Date: Fri, 1 Feb 2019 11:26:26 -0800 Subject: [PATCH 3/4] Return separate `additionalParameters` maps for /token and /authorize. --- README.md | 3 ++- .../main/java/com/rnappauth/RNAppAuthModule.java | 15 +++++++++------ index.d.ts | 14 ++++++++++++-- ios/RNAppAuth.m | 3 ++- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c564c1b8..8aaa73b4 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,8 @@ This is the result from the auth server * **accessToken** - (`string`) the access token * **accessTokenExpirationDate** - (`string`) the token expiration date -* **additionalParameters** - (`Object`) additional url parameters from the auth server +* **authorizeAdditionalParameters** - (`Object`) additional url parameters from the authorizationEndpoint response. +* **tokenAdditionalParameters** - (`Object`) additional url parameters from the tokenEndpoint response. * **idToken** - (`string`) the id token * **refreshToken** - (`string`) the refresh token * **tokenType** - (`string`) the token type, e.g. Bearer diff --git a/android/src/main/java/com/rnappauth/RNAppAuthModule.java b/android/src/main/java/com/rnappauth/RNAppAuthModule.java index 8ecb6c1c..6a665f9e 100644 --- a/android/src/main/java/com/rnappauth/RNAppAuthModule.java +++ b/android/src/main/java/com/rnappauth/RNAppAuthModule.java @@ -447,7 +447,7 @@ private WritableMap tokenResponseToMap(TokenResponse response, AuthorizationResp map.putString("accessTokenExpirationDate", expirationDateString); } - WritableMap additionalParametersMap = Arguments.createMap(); + WritableMap authorizeAdditionalParameters = Arguments.createMap(); if (!authResponse.additionalParameters.isEmpty()) { @@ -455,21 +455,24 @@ private WritableMap tokenResponseToMap(TokenResponse response, AuthorizationResp while(iterator.hasNext()) { String key = iterator.next(); - additionalParametersMap.putString(key, authResponse.additionalParameters.get(key)); + authorizeAdditionalParameters.putString(key, authResponse.additionalParameters.get(key)); } } - if(!this.additionalParametersMap.isEmpty()) { + WritableMap tokenAdditionalParameters = Arguments.createMap(); - Iterator iterator = this.additionalParametersMap.keySet().iterator(); + if (!response.additionalParameters.isEmpty()) { + + Iterator iterator = response.additionalParameters.keySet().iterator(); while(iterator.hasNext()) { String key = iterator.next(); - additionalParametersMap.putString(key, this.additionalParametersMap.get(key)); + tokenAdditionalParameters.putString(key, response.additionalParameters.get(key)); } } - map.putMap("additionalParameters", additionalParametersMap); + map.putMap("authorizeAdditionalParameters", authorizeAdditionalParameters); + map.putMap("tokenAdditionalParameters", tokenAdditionalParameters); map.putString("idToken", response.idToken); map.putString("refreshToken", response.refreshToken); map.putString("tokenType", response.tokenType); diff --git a/index.d.ts b/index.d.ts index 14dbe4b7..3bf1a208 100644 --- a/index.d.ts +++ b/index.d.ts @@ -36,13 +36,23 @@ export type AuthConfiguration = BaseAuthConfiguration & { export interface AuthorizeResult { accessToken: string; accessTokenExpirationDate: string; - additionalParameters?: { [name: string]: string }; + authorizeAdditionalParameters?: { [name: string]: string }; + tokenAdditionalParameters?: { [name: string]: string }; idToken: string; refreshToken: string; tokenType: string; scopes: [string]; } +export interface RefreshResult { + accessToken: string; + accessTokenExpirationDate: string; + additionalParameters?: { [name: string]: string }; + idToken: string; + refreshToken: string; + tokenType: string; +} + export interface RevokeConfiguration { tokenToRevoke: string; sendClientId?: boolean; @@ -57,7 +67,7 @@ export function authorize(config: AuthConfiguration): Promise; export function refresh( config: AuthConfiguration, refreshConfig: RefreshConfiguration -): Promise; +): Promise; export function revoke( config: BaseAuthConfiguration, diff --git a/ios/RNAppAuth.m b/ios/RNAppAuth.m index e4a1d62f..6841529e 100644 --- a/ios/RNAppAuth.m +++ b/ios/RNAppAuth.m @@ -284,7 +284,8 @@ - (NSDictionary*)formatResponse: (OIDTokenResponse*) response return @{@"accessToken": response.accessToken ? response.accessToken : @"", @"accessTokenExpirationDate": response.accessTokenExpirationDate ? [dateFormat stringFromDate:response.accessTokenExpirationDate] : @"", - @"additionalParameters": authResponse.additionalParameters, + @"authorizeAdditionalParameters": authResponse.additionalParameters, + @"tokenAdditionalParameters": response.additionalParameters, @"idToken": response.idToken ? response.idToken : @"", @"refreshToken": response.refreshToken ? response.refreshToken : @"", @"tokenType": response.tokenType ? response.tokenType : @"", From ef34c2df5b52cff46d8231b14cefba4f18f29479 Mon Sep 17 00:00:00 2001 From: Adam Michel Date: Fri, 1 Feb 2019 11:26:38 -0800 Subject: [PATCH 4/4] Add example configuration and notes for Dropbox. --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index 8aaa73b4..7d2942e4 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ These providers implement the OAuth2 spec, but are not OpenID providers, which m * [Uber](https://developer.uber.com/docs/deliveries/guides/three-legged-oauth) ([Example configuration](#uber)) * [Fitbit](https://dev.fitbit.com/build/reference/web-api/oauth2/) ([Example configuration](#fitbit)) +* [Dropbox](https://www.dropbox.com/developers/reference/oauth-guide) ([Example configuration](#dropbox)) ## Why you may want to use this library @@ -716,6 +717,36 @@ await revoke(config, { }); ``` +### Dropbox + +Dropbox provides an OAuth 2.0 endpoint for logging in with a Dropbox user's credentials. You'll need to first [register your Dropbox application here](https://www.dropbox.com/developers/apps/create). + +Please note: + +* Dropbox does not provide a OIDC discovery endpoint, so `serviceConfiguration` is used instead. +* Dropbox OAuth requires a [client secret](#note-about-client-secrets). +* Dropbox OAuth does not allow non-https redirect URLs, so you'll need to use a [Universal Link on iOS](https://developer.apple.com/library/archive/documentation/General/Conceptual/AppSearch/UniversalLinks.html) or write a HTTPS endpoint. +* Dropbox OAuth does not provide refresh tokens or a revoke endpoint. + +```js +const config = { + clientId: 'your-client-id-generated-by-dropbox', + clientSecret: 'your-client-secret-generated-by-dropbox', + redirectUrl: 'https://native-redirect-endpoint/oauth/dropbox', + scopes: [], + serviceConfiguration: { + authorizationEndpoint: 'https://www.dropbox.com/oauth2/authorize', + tokenEndpoint: `https://www.dropbox.com/oauth2/token`, + }, + useNonce: false, + usePKCE: false, +}; + +// Log in to get an authentication token +const authState = await authorize(config); +const dropboxUID = authState.tokenAdditionalParameters.account_id; +``` + ## Contributors Thanks goes to these wonderful people