Skip to content

Commit 99f0fa3

Browse files
author
Kadi Kraman
authored
Merge pull request #540 from FormidableLabs/echo-health-echo-fork
Support manual authorization code exchange
2 parents eed4d8f + b9e3bdb commit 99f0fa3

File tree

8 files changed

+134
-14
lines changed

8 files changed

+134
-14
lines changed

.travis.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
notifications:
22
email: false
33

4-
branches:
5-
only:
6-
- master
7-
- /^v\d+\.\d+\.\d+/
8-
94
language: node_js
105
cache:
116
yarn: true

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,11 @@ with optional overrides.
130130
- **register** - (`{ [key: string]: value }`) headers to be passed during registration request.
131131
- **useNonce** - (`boolean`) _IOS_ (default: true) optionally allows not sending the nonce parameter, to support non-compliant providers
132132
- **usePKCE** - (`boolean`) (default: true) optionally allows not sending the code_challenge parameter and skipping PKCE code verification, to support non-compliant providers.
133+
- **skipCodeExchange** - (`boolean`) (default: false) just return the authorization response, instead of automatically exchanging the authorization code. This is useful if this exchange needs to be done manually (not client-side)
133134

134135
#### result
135136

136-
This is the result from the auth server
137+
This is the result from the auth server:
137138

138139
- **accessToken** - (`string`) the access token
139140
- **accessTokenExpirationDate** - (`string`) the token expiration date
@@ -143,6 +144,7 @@ This is the result from the auth server
143144
- **refreshToken** - (`string`) the refresh token
144145
- **tokenType** - (`string`) the token type, e.g. Bearer
145146
- **scopes** - ([`string`]) the scopes the user has agreed to be granted
147+
- **authorizationCode** - (`string`) the authorization code (only if `skipCodeExchange=true`)
146148

147149
### `refresh`
148150

@@ -332,6 +334,19 @@ Add the following code to `AppDelegate.m` (to support iOS 10 and below)
332334
+ }
333335
```
334336
337+
If you want to support universal links, add the following to `AppDelegate.m` under `continueUserActivity`
338+
339+
```diff
340+
+ if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
341+
+ if (self.authorizationFlowManagerDelegate) {
342+
+ BOOL resumableAuth = [self.authorizationFlowManagerDelegate resumeExternalUserAgentFlowWithURL:userActivity.webpageURL];
343+
+ if (resumableAuth) {
344+
+ return YES;
345+
+ }
346+
+ }
347+
+ }
348+
```
349+
335350
#### Integration of the library with a Swift iOS project
336351
337352
The approach mentioned should work with Swift. In this case one should make `AppDelegate` conform to `RNAppAuthAuthorizationFlowManager`. Note that this is not tested/guaranteed by the maintainers.

android/src/main/java/com/rnappauth/RNAppAuthModule.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public class RNAppAuthModule extends ReactContextBaseJavaModule implements Activ
6262
private final ReactApplicationContext reactContext;
6363
private Promise promise;
6464
private boolean dangerouslyAllowInsecureHttpRequests;
65+
private Boolean skipCodeExchange;
6566
private String clientAuthMethod = "basic";
6667
private Map<String, String> registrationRequestHeaders = null;
6768
private Map<String, String> authorizationRequestHeaders = null;
@@ -216,6 +217,7 @@ public void authorize(
216217
final ReadableArray scopes,
217218
final ReadableMap additionalParameters,
218219
final ReadableMap serviceConfiguration,
220+
final Boolean skipCodeExchange,
219221
final Boolean usePKCE,
220222
final String clientAuthMethod,
221223
final boolean dangerouslyAllowInsecureHttpRequests,
@@ -233,6 +235,7 @@ public void authorize(
233235
this.additionalParametersMap = additionalParametersMap;
234236
this.clientSecret = clientSecret;
235237
this.clientAuthMethod = clientAuthMethod;
238+
this.skipCodeExchange = skipCodeExchange;
236239

237240
// when serviceConfiguration is provided, we don't need to hit up the OpenID well-known id endpoint
238241
if (serviceConfiguration != null || hasServiceConfiguration(issuer)) {
@@ -392,6 +395,13 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode,
392395
return;
393396
}
394397

398+
if (this.skipCodeExchange) {
399+
WritableMap map = TokenResponseFactory.authorizationResponseToMap(response);
400+
promise.resolve(map);
401+
return;
402+
}
403+
404+
395405
final Promise authorizePromise = this.promise;
396406
final AppAuthConfiguration configuration = createAppAuthConfiguration(
397407
createConnectionBuilder(this.dangerouslyAllowInsecureHttpRequests, this.tokenRequestHeaders)

android/src/main/java/com/rnappauth/utils/TokenResponseFactory.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,26 @@ public static final WritableMap tokenResponseToMap(TokenResponse response, Autho
6464
}
6565

6666

67+
return map;
68+
}
69+
70+
71+
/*
72+
* Read raw authorization into a React Native map to be passed down the bridge
73+
*/
74+
public static final WritableMap authorizationResponseToMap(AuthorizationResponse authResponse) {
75+
WritableMap map = Arguments.createMap();
76+
map.putString("authorizationCode", authResponse.authorizationCode);
77+
map.putString("accessToken", authResponse.accessToken);
78+
map.putMap("additionalParameters", MapUtil.createAdditionalParametersMap(authResponse.additionalParameters));
79+
map.putString("idToken", authResponse.idToken);
80+
map.putString("tokenType", authResponse.tokenType);
81+
map.putArray("scopes", createScopeArray(authResponse.scope));
82+
83+
if (authResponse.accessTokenExpirationTime != null) {
84+
map.putString("accessTokenExpirationTime", DateUtil.formatTimestamp(authResponse.accessTokenExpirationTime));
85+
}
86+
6787
return map;
6888
}
6989
}

index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export type AuthConfiguration = BaseAuthConfiguration & {
7272
useNonce?: boolean;
7373
usePKCE?: boolean;
7474
warmAndPrefetchChrome?: boolean;
75+
skipCodeExchange?: boolean;
7576
};
7677

7778
export interface AuthorizeResult {
@@ -83,6 +84,7 @@ export interface AuthorizeResult {
8384
refreshToken: string;
8485
tokenType: string;
8586
scopes: string[];
87+
authorizationCode: string;
8688
}
8789

8890
export interface RefreshResult {

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export const authorize = ({
155155
clientAuthMethod = 'basic',
156156
dangerouslyAllowInsecureHttpRequests = false,
157157
customHeaders,
158+
skipCodeExchange = false,
158159
}) => {
159160
validateIssuerOrServiceConfigurationEndpoints(issuer, serviceConfiguration);
160161
validateClientId(clientId);
@@ -170,6 +171,7 @@ export const authorize = ({
170171
scopes,
171172
additionalParameters,
172173
serviceConfiguration,
174+
skipCodeExchange,
173175
];
174176

175177
if (Platform.OS === 'android') {

index.spec.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ describe('AppAuth', () => {
4141
useNonce: true,
4242
usePKCE: true,
4343
customHeaders: null,
44+
skipCodeExchange: false,
4445
};
4546

4647
const registerConfig = {
@@ -380,11 +381,37 @@ describe('AppAuth', () => {
380381
config.scopes,
381382
config.additionalParameters,
382383
config.serviceConfiguration,
384+
config.skipCodeExchange,
383385
config.useNonce,
384386
config.usePKCE
385387
);
386388
});
387389

390+
it('calls the native wrapper with the default value `false`, `true`, `true`', () => {
391+
authorize({
392+
issuer: 'test-issuer',
393+
redirectUrl: 'test-redirectUrl',
394+
clientId: 'test-clientId',
395+
clientSecret: 'test-clientSecret',
396+
customHeaders: null,
397+
additionalParameters: null,
398+
serviceConfiguration: null,
399+
scopes: ['openid'],
400+
});
401+
expect(mockAuthorize).toHaveBeenCalledWith(
402+
'test-issuer',
403+
'test-redirectUrl',
404+
'test-clientId',
405+
'test-clientSecret',
406+
['openid'],
407+
null,
408+
null,
409+
false,
410+
true,
411+
true
412+
);
413+
});
414+
388415
describe('Android-specific', () => {
389416
beforeEach(() => {
390417
require('react-native').Platform.OS = 'android';
@@ -404,6 +431,7 @@ describe('AppAuth', () => {
404431
config.scopes,
405432
config.additionalParameters,
406433
config.serviceConfiguration,
434+
config.skipCodeExchange,
407435
config.usePKCE,
408436
config.clientAuthMethod,
409437
false,
@@ -421,6 +449,7 @@ describe('AppAuth', () => {
421449
config.scopes,
422450
config.additionalParameters,
423451
config.serviceConfiguration,
452+
false,
424453
config.usePKCE,
425454
config.clientAuthMethod,
426455
false,
@@ -438,6 +467,7 @@ describe('AppAuth', () => {
438467
config.scopes,
439468
config.additionalParameters,
440469
config.serviceConfiguration,
470+
false,
441471
config.usePKCE,
442472
config.clientAuthMethod,
443473
true,
@@ -464,6 +494,7 @@ describe('AppAuth', () => {
464494
config.scopes,
465495
config.additionalParameters,
466496
config.serviceConfiguration,
497+
false,
467498
config.usePKCE,
468499
config.clientAuthMethod,
469500
false,
@@ -619,6 +650,7 @@ describe('AppAuth', () => {
619650
config.scopes,
620651
config.additionalParameters,
621652
config.serviceConfiguration,
653+
false,
622654
config.usePKCE,
623655
config.clientAuthMethod,
624656
false,
@@ -643,6 +675,7 @@ describe('AppAuth', () => {
643675
config.scopes,
644676
config.additionalParameters,
645677
config.serviceConfiguration,
678+
false,
646679
true,
647680
true
648681
);
@@ -659,6 +692,7 @@ describe('AppAuth', () => {
659692
config.additionalParameters,
660693
config.serviceConfiguration,
661694
false,
695+
false,
662696
true
663697
);
664698
});
@@ -679,6 +713,7 @@ describe('AppAuth', () => {
679713
config.scopes,
680714
config.additionalParameters,
681715
config.serviceConfiguration,
716+
config.skipCodeExchange,
682717
config.useNonce,
683718
true
684719
);
@@ -694,6 +729,7 @@ describe('AppAuth', () => {
694729
config.scopes,
695730
config.additionalParameters,
696731
config.serviceConfiguration,
732+
config.skipCodeExchange,
697733
config.useNonce,
698734
false
699735
);

ios/RNAppAuth.m

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ - (dispatch_queue_t)methodQueue
8888
scopes: (NSArray *) scopes
8989
additionalParameters: (NSDictionary *_Nullable) additionalParameters
9090
serviceConfiguration: (NSDictionary *_Nullable) serviceConfiguration
91+
skipCodeExchange: (BOOL) skipCodeExchange
9192
useNonce: (BOOL *) useNonce
9293
usePKCE: (BOOL *) usePKCE
9394
resolve: (RCTPromiseResolveBlock) resolve
@@ -104,6 +105,7 @@ - (dispatch_queue_t)methodQueue
104105
useNonce: useNonce
105106
usePKCE: usePKCE
106107
additionalParameters: additionalParameters
108+
skipCodeExchange: skipCodeExchange
107109
resolve: resolve
108110
reject: reject];
109111
} else {
@@ -121,6 +123,7 @@ - (dispatch_queue_t)methodQueue
121123
useNonce: useNonce
122124
usePKCE: usePKCE
123125
additionalParameters: additionalParameters
126+
skipCodeExchange: skipCodeExchange
124127
resolve: resolve
125128
reject: reject];
126129
}];
@@ -259,6 +262,7 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration
259262
useNonce: (BOOL *) useNonce
260263
usePKCE: (BOOL *) usePKCE
261264
additionalParameters: (NSDictionary *_Nullable) additionalParameters
265+
skipCodeExchange: (BOOL) skipCodeExchange
262266
resolve: (RCTPromiseResolveBlock) resolve
263267
reject: (RCTPromiseRejectBlock) reject
264268
{
@@ -296,24 +300,39 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration
296300
taskId = UIBackgroundTaskInvalid;
297301
}];
298302

299-
_currentSession = [OIDAuthState authStateByPresentingAuthorizationRequest:request
303+
if (skipCodeExchange) {
304+
_currentSession = [OIDAuthorizationService presentAuthorizationRequest:request
300305
presentingViewController:appDelegate.window.rootViewController
301-
callback:^(OIDAuthState *_Nullable authState,
302-
NSError *_Nullable error) {
306+
callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse, NSError *_Nullable error) {
303307
typeof(self) strongSelf = weakSelf;
304308
strongSelf->_currentSession = nil;
305309
[UIApplication.sharedApplication endBackgroundTask:taskId];
306310
taskId = UIBackgroundTaskInvalid;
307-
if (authState) {
308-
resolve([self formatResponse:authState.lastTokenResponse
309-
withAuthResponse:authState.lastAuthorizationResponse]);
311+
if (authorizationResponse) {
312+
resolve([self formatAuthorizationResponse:authorizationResponse]);
310313
} else {
311314
reject(@"authentication_failed", [error localizedDescription], error);
312315
}
313-
}]; // end [OIDAuthState authStateByPresentingAuthorizationRequest:request
316+
}]; // end [OIDAuthState presentAuthorizationRequest:request
317+
} else {
318+
_currentSession = [OIDAuthState authStateByPresentingAuthorizationRequest:request
319+
presentingViewController:appDelegate.window.rootViewController
320+
callback:^(OIDAuthState *_Nullable authState,
321+
NSError *_Nullable error) {
322+
typeof(self) strongSelf = weakSelf;
323+
strongSelf->_currentSession = nil;
324+
[UIApplication.sharedApplication endBackgroundTask:taskId];
325+
taskId = UIBackgroundTaskInvalid;
326+
if (authState) {
327+
resolve([self formatResponse:authState.lastTokenResponse
328+
withAuthResponse:authState.lastAuthorizationResponse]);
329+
} else {
330+
reject(@"authentication_failed", [error localizedDescription], error);
331+
}
332+
}]; // end [OIDAuthState authStateByPresentingAuthorizationRequest:request
333+
}
314334
}
315335

316-
317336
/*
318337
* Refresh a token with provided OIDServiceConfiguration
319338
*/
@@ -350,6 +369,27 @@ - (void)refreshWithConfiguration: (OIDServiceConfiguration *)configuration
350369
}];
351370
}
352371

372+
373+
/*
374+
* Take raw OIDAuthorizationResponse and turn it to response format to pass to JavaScript caller
375+
*/
376+
- (NSDictionary*)formatAuthorizationResponse: (OIDAuthorizationResponse*) response {
377+
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
378+
dateFormat.timeZone = [NSTimeZone timeZoneWithAbbreviation: @"UTC"];
379+
[dateFormat setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
380+
[dateFormat setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"];
381+
382+
return @{@"authorizationCode": response.authorizationCode ? response.authorizationCode : @"",
383+
@"state": response.state ? response.state : @"",
384+
@"accessToken": response.accessToken ? response.accessToken : @"",
385+
@"accessTokenExpirationDate": response.accessTokenExpirationDate ? [dateFormat stringFromDate:response.accessTokenExpirationDate] : @"",
386+
@"tokenType": response.tokenType ? response.tokenType : @"",
387+
@"idToken": response.idToken ? response.idToken : @"",
388+
@"scopes": response.scope ? [response.scope componentsSeparatedByString:@" "] : [NSArray new],
389+
@"additionalParameters": response.additionalParameters,
390+
};
391+
}
392+
353393
/*
354394
* Take raw OIDTokenResponse and turn it to a token response format to pass to JavaScript caller
355395
*/

0 commit comments

Comments
 (0)