Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added clearSession iOS Safari Method #65

Merged
merged 7 commits into from
Aug 18, 2017
Merged
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
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);
}
}