Skip to content

Commit

Permalink
Merge pull request #65 from auth0/added-clearsession-safari
Browse files Browse the repository at this point in the history
Added clearSession iOS Safari Method
  • Loading branch information
hzalaz committed Aug 18, 2017
2 parents 7a87224 + 493ed73 commit f48d415
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 32 deletions.
2 changes: 1 addition & 1 deletion android/src/main/java/com/auth0/react/A0Auth0Module.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public Map<String, Object> getConstants() {
}

@ReactMethod
public void showUrl(String url, Callback callback) {
public void showUrl(String url, boolean closeOnLoad, Callback callback) {
final Activity activity = getCurrentActivity();

this.callback = callback;
Expand Down
6 changes: 6 additions & 0 deletions auth/__tests__/__snapshots__/index.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ Array [
]
`;

exports[`auth logoutUrl should return default logout url 1`] = `"https://samples.auth0.com/v2/logout"`;

exports[`auth logoutUrl should return logout url with extra parameters 1`] = `"https://samples.auth0.com/v2/logout?federated=true&client_id=CLIENT_ID"`;

exports[`auth logoutUrl should return logout url with skipping unknown parameters 1`] = `"https://samples.auth0.com/v2/logout?federated=true"`;

exports[`auth password realm should handle oauth error 1`] = `[invalid_request: Invalid grant]`;

exports[`auth password realm should handle unexpected error 1`] = `[a0.response.invalid: Internal Server Error]`;
Expand Down
21 changes: 21 additions & 0 deletions auth/__tests__/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,27 @@ describe('auth', () => {
});
});

describe('logoutUrl', () => {
it('should return default logout url', () => {
expect(auth.logoutUrl({})).toMatchSnapshot();
});

it('should return logout url with extra parameters', () => {
expect(auth.logoutUrl({
federated: true,
clientId: 'CLIENT_ID',
redirectTo: 'https://auth0.com'
})).toMatchSnapshot();
});

it('should return logout url with skipping unknown parameters', () => {
expect(auth.logoutUrl({
federated: true,
shouldNotBeThere: 'really'
})).toMatchSnapshot();
});
});

describe('code exchange', () => {
it('should send correct payload', async () => {
fetchMock.postOnce('https://samples.auth0.com/oauth/token', tokens);
Expand Down
23 changes: 23 additions & 0 deletions auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,29 @@ export default class Auth {
return this.client.url('/authorize', {...query, client_id: this.clientId}, true);
}

/**
* Builds the full logout endpoint url in the Authorization Server (AS) with given parameters.
*
* @param {Object} parameters parameters to send to `/v2/logout`
* @param {Boolean} [parameters.federated] if the logout should include removing session for federated IdP.
* @param {String} [parameters.clientId] client identifier of the one requesting the logout
* @param {String} [parameters.returnTo] url where the user is redirected to after logout. It must be declared in you Auth0 Dashboard
* @returns {String} logout url with specified parameters
* @see https://auth0.com/docs/api/authentication#logout
*
* @memberof Auth
*/
logoutUrl(parameters = {}) {
const query = apply({
parameters: {
federated: { required: false },
clientId: { required: false, toName: 'client_id' },
returnTo: { required: false }
}
}, parameters);
return this.client.url('/v2/logout', {...query});
}

/**
* Exchanges a code obtained via `/authorize` (w/PKCE) for the user's tokens
*
Expand Down
65 changes: 36 additions & 29 deletions ios/A0Auth0.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
@interface A0Auth0 () <SFSafariViewControllerDelegate>
@property (weak, nonatomic) SFSafariViewController *last;
@property (copy, nonatomic) RCTResponseSenderBlock sessionCallback;
@property (assign, nonatomic) BOOL closeOnLoad;
@end

@implementation A0Auth0
Expand All @@ -28,14 +29,9 @@ - (dispatch_queue_t)methodQueue
[self terminateWithError:nil dismissing:YES animated:YES];
}

RCT_EXPORT_METHOD(showUrl:(NSString *)urlString callback:(RCTResponseSenderBlock)callback) {
NSURL *url = [NSURL URLWithString:urlString];
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
SFSafariViewController *controller = [[SFSafariViewController alloc] initWithURL:url];
controller.delegate = self;
[self terminateWithError:RCTMakeError(@"Only one Safari can be visible", nil, nil) dismissing:YES animated:NO];
[window.rootViewController presentViewController:controller animated:YES completion:nil];
self.last = controller;
RCT_EXPORT_METHOD(showUrl:(NSString *)urlString closeOnLoad:(BOOL)closeOnLoad callback:(RCTResponseSenderBlock)callback) {
[self presentSafariWithURL:[NSURL URLWithString:urlString]];
self.closeOnLoad = closeOnLoad;
self.sessionCallback = callback;
}

Expand All @@ -44,34 +40,44 @@ - (dispatch_queue_t)methodQueue
}

- (NSDictionary *)constantsToExport {
return @{ @"bundleIdentifier": [[NSBundle mainBundle] bundleIdentifier] };
return @{ @"bundleIdentifier": [[NSBundle mainBundle] bundleIdentifier] };
}

#pragma mark - Internal methods

- (void)presentSafariWithURL:(NSURL *)url {
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
SFSafariViewController *controller = [[SFSafariViewController alloc] initWithURL:url];
controller.delegate = self;
[self terminateWithError:RCTMakeError(@"Only one Safari can be visible", nil, nil) dismissing:YES animated:NO];
[window.rootViewController presentViewController:controller animated:YES completion:nil];
self.last = controller;
}

- (void)terminateWithError:(id)error dismissing:(BOOL)dismissing animated:(BOOL)animated {
RCTResponseSenderBlock callback = self.sessionCallback ? self.sessionCallback : ^void(NSArray *_unused) {};
if (dismissing) {
[self.last.presentingViewController dismissViewControllerAnimated:animated
completion:^{
if (error) {
callback(@[error]);
}
}];
completion:^{
if (error) {
callback(@[error]);
}
}];
} else if (error) {
callback(@[error]);
}
self.sessionCallback = nil;
self.last = nil;
self.closeOnLoad = NO;
}

- (NSString *)randomValue {
NSMutableData *data = [NSMutableData dataWithLength:32];
int result __attribute__((unused)) = SecRandomCopyBytes(kSecRandomDefault, 32, data.mutableBytes);
NSString *value = [[[[data base64EncodedStringWithOptions:0]
stringByReplacingOccurrencesOfString:@"+" withString:@"-"]
stringByReplacingOccurrencesOfString:@"/" withString:@"_"]
stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"="]];
stringByReplacingOccurrencesOfString:@"/" withString:@"_"]
stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"="]];
return value;
}

Expand Down Expand Up @@ -101,31 +107,32 @@ - (NSString *)sign:(NSString*)value {

- (NSDictionary *)generateOAuthParameters {
NSString *verifier = [self randomValue];
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
return @{
@"verifier": verifier,
@"code_challenge": [self sign:verifier],
@"code_challenge_method": @"S256",
@"state": [self randomValue]
};
@"verifier": verifier,
@"code_challenge": [self sign:verifier],
@"code_challenge_method": @"S256",
@"state": [self randomValue]
};
}

#pragma mark - SFSafariViewControllerDelegate

- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller {
NSDictionary *error = @{
@"error": @"a0.session.user_cancelled",
@"error_description": @"User cancelled the Auth"
};
@"error": @"a0.session.user_cancelled",
@"error_description": @"User cancelled the Auth"
};
[self terminateWithError:error dismissing:NO animated:NO];
}

- (void)safariViewController:(SFSafariViewController *)controller didCompleteInitialLoad:(BOOL)didLoadSuccessfully {
if (!didLoadSuccessfully) {
if (self.closeOnLoad && didLoadSuccessfully) {
[self terminateWithError:nil dismissing:YES animated:YES];
} else if (!didLoadSuccessfully) {
NSDictionary *error = @{
@"error": @"a0.session.failed_load",
@"error_description": @"Failed to load authorize url"
};
@"error": @"a0.session.failed_load",
@"error_description": @"Failed to load url"
};
[self terminateWithError:error dismissing:YES animated:YES];
}
}
Expand Down
4 changes: 2 additions & 2 deletions webauth/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ const { A0Auth0 } = NativeModules;

export default class Agent {

show(url) {
show(url, closeOnLoad = false) {
return new Promise((resolve, reject) => {
const urlHandler = (event) => {
A0Auth0.hide();
Linking.removeEventListener('url', urlHandler);
resolve(event.url);
};
Linking.addEventListener('url', urlHandler);
A0Auth0.showUrl(url, (err) => {
A0Auth0.showUrl(url, closeOnLoad, (err) => {
Linking.removeEventListener('url', urlHandler);
reject(err);
});
Expand Down
27 changes: 27 additions & 0 deletions webauth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,31 @@ export default class WebAuth {
});
});
}

/**
* Removes Auth0 session and optionally remove the Identity Provider session.
* In iOS it will use `SFSafariViewController`
*
* @param {Object} parameters parameters to send
* @param {Bool} [parameters.federated] Optionally remove the IdP session.
* @returns {Promise}
* @see https://auth0.com/docs/logout
*
* @memberof WebAuth
*/
clearSession(options = {}) {
if (Platform.OS !== 'ios') {
return Promise.reject(new AuthError({
json: {
error: 'a0.platform.not_available',
error_description: `Cannot perform operation in platform ${Platform.OS}`
},
status: 0
}));
}
const { client, agent } = this;
const federated = options.federated || false;
const logoutUrl = client.logoutUrl(options);
return agent.show(logoutUrl, true);
}
}

0 comments on commit f48d415

Please sign in to comment.