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

Implement OIDC RP-Initiated Logout #259

Merged
merged 7 commits into from
Jun 11, 2019
72 changes: 72 additions & 0 deletions AppAuth.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Source/AppAuth.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
#import "OIDTokenResponse.h"
#import "OIDTokenUtilities.h"
#import "OIDURLSessionProvider.h"
#import "OIDEndSessionRequest.h"
#import "OIDEndSessionResponse.h"

#if TARGET_OS_TV
#elif TARGET_OS_WATCH
Expand Down
2 changes: 2 additions & 0 deletions Source/Framework/AppAuth.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ FOUNDATION_EXPORT const unsigned char AppAuthVersionString[];
#import <AppAuth/OIDTokenResponse.h>
#import <AppAuth/OIDTokenUtilities.h>
#import <AppAuth/OIDURLSessionProvider.h>
#import <AppAuth/OIDEndSessionRequest.h>
#import <AppAuth/OIDEndSessionResponse.h>

#if TARGET_OS_TV
#elif TARGET_OS_WATCH
Expand Down
23 changes: 23 additions & 0 deletions Source/OIDAuthorizationService.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
@class OIDAuthorization;
@class OIDAuthorizationRequest;
@class OIDAuthorizationResponse;
@class OIDEndSessionRequest;
@class OIDEndSessionResponse;
@class OIDRegistrationRequest;
@class OIDRegistrationResponse;
@class OIDServiceConfiguration;
Expand All @@ -47,6 +49,13 @@ typedef void (^OIDDiscoveryCallback)(OIDServiceConfiguration *_Nullable configur
typedef void (^OIDAuthorizationCallback)(OIDAuthorizationResponse *_Nullable authorizationResponse,
NSError *_Nullable error);

/*! @brief Block used as a callback for the end-session request of @c OIDAuthorizationService.
@param endSessionResponse The end-session response, if available.
@param error The error if an error occurred.
*/
typedef void (^OIDEndSessionCallback)(OIDEndSessionResponse *_Nullable endSessionResponse,
NSError *_Nullable error);

/*! @brief Represents the type of block used as a callback for various methods of
@c OIDAuthorizationService.
@param tokenResponse The token response, if available.
Expand Down Expand Up @@ -120,6 +129,20 @@ typedef void (^OIDRegistrationCompletion)(OIDRegistrationResponse *_Nullable reg
externalUserAgent:(id<OIDExternalUserAgent>)externalUserAgent
callback:(OIDAuthorizationCallback)callback;

/*! @brief Perform a logout request.
@param request The end-session logout request.
@param externalUserAgent Generic external user-agent that can present user-agent requests.
@param callback The method called when the request has completed or failed.
@return A @c OIDExternalUserAgentSession instance which will terminate when it
receives a @c OIDExternalUserAgentSession.cancel message, or after processing a
@c OIDExternalUserAgentSession.resumeExternalUserAgentFlowWithURL: message.
@see http://openid.net/specs/openid-connect-session-1_0.html#RPLogout
*/
+ (id<OIDExternalUserAgentSession>)
presentEndSessionRequest:(OIDEndSessionRequest *)request
externalUserAgent:(id<OIDExternalUserAgent>)externalUserAgent
callback:(OIDEndSessionCallback)callback;

/*! @brief Performs a token request.
@param request The token request.
@param callback The method called when the request has completed or failed.
Expand Down
140 changes: 138 additions & 2 deletions Source/OIDAuthorizationService.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
#import "OIDAuthorizationRequest.h"
#import "OIDAuthorizationResponse.h"
#import "OIDDefines.h"
#import "OIDEndSessionRequest.h"
#import "OIDEndSessionResponse.h"
#import "OIDErrorUtilities.h"
#import "OIDExternalUserAgent.h"
#import "OIDExternalUserAgentSession.h"
Expand Down Expand Up @@ -92,9 +94,14 @@ - (void)cancel {
}];
}

- (BOOL)shouldHandleURL:(NSURL *)URL {
/*! @brief Does the redirection URL equal another URL down to the path component?
@param URL The first redirect URI to compare.
@param redirectonURL The second redirect URI to compare.
Copy link
Collaborator

@julienbodet julienbodet Jul 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo redirectionURL

@return YES if the URLs match down to the path level (query params are ignored).
*/
+ (BOOL)URL:(NSURL *)URL matchesRedirectonURL:(NSURL *)redirectonURL {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo matchesRedirectionURL

NSURL *standardizedURL = [URL standardizedURL];
NSURL *standardizedRedirectURL = [_request.redirectURL standardizedURL];
NSURL *standardizedRedirectURL = [redirectonURL standardizedURL];
Copy link
Collaborator

@julienbodet julienbodet Jul 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[redirectionURL standardizedURL]


return [standardizedURL.scheme caseInsensitiveCompare:standardizedRedirectURL.scheme] == NSOrderedSame
&& OIDIsEqualIncludingNil(standardizedURL.user, standardizedRedirectURL.user)
Expand All @@ -104,6 +111,10 @@ - (BOOL)shouldHandleURL:(NSURL *)URL {
&& OIDIsEqualIncludingNil(standardizedURL.path, standardizedRedirectURL.path);
}

- (BOOL)shouldHandleURL:(NSURL *)URL {
return [[self class] URL:URL matchesRedirectonURL:_request.redirectURL];
}

- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL {
// rejects URLs that don't match redirect (these may be completely unrelated to the authorization)
if (![self shouldHandleURL:URL]) {
Expand Down Expand Up @@ -178,6 +189,121 @@ - (void)didFinishWithResponse:(nullable OIDAuthorizationResponse *)response

@end

@interface OIDEndSessionImplementation : NSObject<OIDExternalUserAgentSession> {
// private variables
OIDEndSessionRequest *_request;
id<OIDExternalUserAgent> _externalUserAgent;
OIDEndSessionCallback _pendingEndSessionCallback;
}
- (instancetype)init NS_UNAVAILABLE;

- (instancetype)initWithRequest:(OIDEndSessionRequest *)request
NS_DESIGNATED_INITIALIZER;
@end


@implementation OIDEndSessionImplementation

- (instancetype)initWithRequest:(OIDEndSessionRequest *)request {
self = [super init];
if (self) {
_request = [request copy];
}
return self;
}

- (void)presentAuthorizationWithExternalUserAgent:(id<OIDExternalUserAgent>)externalUserAgent
callback:(OIDEndSessionCallback)authorizationFlowCallback {
_externalUserAgent = externalUserAgent;
_pendingEndSessionCallback = authorizationFlowCallback;
BOOL authorizationFlowStarted =
[_externalUserAgent presentExternalUserAgentRequest:_request session:self];
if (!authorizationFlowStarted) {
NSError *safariError = [OIDErrorUtilities errorWithCode:OIDErrorCodeSafariOpenError
Copy link
Collaborator

@julienbodet julienbodet Jul 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is not directly related to this PR, but I wonder if this error type should be renamed to something like OIDErrorCodeBrowserOpenError or OIDErrorCodeExternalUserAgentOpenError since AppAuth now supports other browsers? The description is also mentioning Safari.

underlyingError:nil
description:@"Unable to open Safari."];
[self didFinishWithResponse:nil error:safariError];
}
}

- (void)cancel {
[_externalUserAgent dismissExternalUserAgentAnimated:YES completion:^{
NSError *error = [OIDErrorUtilities
errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow
underlyingError:nil
description:nil];
[self didFinishWithResponse:nil error:error];
}];
}

- (BOOL)shouldHandleURL:(NSURL *)URL {
// The logic of when to handle the URL is the same as for authorization requests: should match
// down to the path component.
return [[OIDAuthorizationSession class] URL:URL
matchesRedirectonURL:_request.postLogoutRedirectURL];
}

- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL {
// rejects URLs that don't match redirect (these may be completely unrelated to the authorization)
if (![self shouldHandleURL:URL]) {
return NO;
}
// checks for an invalid state
if (!_pendingEndSessionCallback) {
[NSException raise:OIDOAuthExceptionInvalidAuthorizationFlow
format:@"%@", OIDOAuthExceptionInvalidAuthorizationFlow, nil];
}


NSError *error;
OIDEndSessionResponse *response = nil;

OIDURLQueryComponent *query = [[OIDURLQueryComponent alloc] initWithURL:URL];
response = [[OIDEndSessionResponse alloc] initWithRequest:_request
parameters:query.dictionaryValue];

// verifies that the state in the response matches the state in the request, or both are nil
if (!OIDIsEqualIncludingNil(_request.state, response.state)) {
NSMutableDictionary *userInfo = [query.dictionaryValue mutableCopy];
userInfo[NSLocalizedDescriptionKey] =
[NSString stringWithFormat:@"State mismatch, expecting %@ but got %@ in authorization "
"response %@",
_request.state,
response.state,
response];
response = nil;
error = [NSError errorWithDomain:OIDOAuthAuthorizationErrorDomain
code:OIDErrorCodeOAuthAuthorizationClientError
userInfo:userInfo];
}

[_externalUserAgent dismissExternalUserAgentAnimated:YES completion:^{
[self didFinishWithResponse:response error:error];
}];

return YES;
}

- (void)failExternalUserAgentFlowWithError:(NSError *)error {
[self didFinishWithResponse:nil error:error];
}

/*! @brief Invokes the pending callback and performs cleanup.
@param response The authorization response, if any to return to the callback.
@param error The error, if any, to return to the callback.
*/
- (void)didFinishWithResponse:(nullable OIDEndSessionResponse *)response
error:(nullable NSError *)error {
OIDEndSessionCallback callback = _pendingEndSessionCallback;
_pendingEndSessionCallback = nil;
_externalUserAgent = nil;
if (callback) {
callback(response, error);
}
}

@end

@implementation OIDAuthorizationService

+ (void)discoverServiceConfigurationForIssuer:(NSURL *)issuerURL
Expand Down Expand Up @@ -272,6 +398,16 @@ + (void)discoverServiceConfigurationForDiscoveryURL:(NSURL *)discoveryURL
return flowSession;
}

+ (id<OIDExternalUserAgentSession>)
presentEndSessionRequest:(OIDEndSessionRequest *)request
externalUserAgent:(id<OIDExternalUserAgent>)externalUserAgent
callback:(OIDEndSessionCallback)callback {
OIDEndSessionImplementation *flowSession =
[[OIDEndSessionImplementation alloc] initWithRequest:request];
[flowSession presentAuthorizationWithExternalUserAgent:externalUserAgent callback:callback];
return flowSession;
}

#pragma mark - Token Endpoint

+ (void)performTokenRequest:(OIDTokenRequest *)request callback:(OIDTokenCallback)callback {
Expand Down
107 changes: 107 additions & 0 deletions Source/OIDEndSessionRequest.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*! @file OIDEndSessionRequest.h
@brief AppAuth iOS SDK
@copyright
Copyright 2017 The AppAuth Authors. All Rights Reserved.
@copydetails
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

#import <Foundation/Foundation.h>

#import "OIDExternalUserAgentRequest.h"

@class OIDServiceConfiguration;

NS_ASSUME_NONNULL_BEGIN

@interface OIDEndSessionRequest : NSObject
<NSCopying, NSSecureCoding, OIDExternalUserAgentRequest>

/*! @brief The service's configuration.
@remarks This configuration specifies how to connect to a particular OAuth provider.
Configurations may be created manually, or via an OpenID Connect Discovery Document.
*/
@property(nonatomic, readonly) OIDServiceConfiguration *configuration;

/*! @brief The client's redirect URI.
@remarks post_logout_redirect_uri
@see http://openid.net/specs/openid-connect-session-1_0.html#RPLogout
*/
@property(nonatomic, readonly, nullable) NSURL *postLogoutRedirectURL;

/*! @brief Previously issued ID Token passed to the end session endpoint as a hint about the End-User's current authenticated
session with the Client
@remarks id_token_hint
@see http://openid.net/specs/openid-connect-session-1_0.html#RPLogout
*/
@property(nonatomic, readonly, nullable) NSString *idTokenHint;

/*! @brief An opaque value used by the client to maintain state between the request and callback.
@remarks state
@discussion If this value is not explicitly set, this library will automatically add state and
perform appropriate validation of the state in the authorization response. It is recommended
that the default implementation of this parameter be used wherever possible. Typically used
to prevent CSRF attacks, as recommended in RFC6819 Section 5.3.5.
@see http://openid.net/specs/openid-connect-session-1_0.html#RPLogout
*/
@property(nonatomic, readonly, nullable) NSString *state;

/*! @brief The client's additional authorization parameters.
@see https://tools.ietf.org/html/rfc6749#section-3.1
*/
@property(nonatomic, readonly, nullable) NSDictionary<NSString *, NSString *> *additionalParameters;

/*! @internal
@brief Unavailable. Please use @c initWithConfiguration:clientId:scopes:redirectURL:additionalParameters:.
*/
- (instancetype)init NS_UNAVAILABLE;

/*! @brief Creates an authorization request with opinionated defaults (a secure @c state).
@param configuration The service's configuration.
@param idTokenHint The previously issued ID Token
@param postLogoutRedirectURL The client's post-logout redirect URI.
callback.
@param additionalParameters The client's additional authorization parameters.
*/
- (instancetype)
initWithConfiguration:(OIDServiceConfiguration *)configuration
idTokenHint:(NSString *)idTokenHint
postLogoutRedirectURL:(NSURL *)postLogoutRedirectURL
additionalParameters:(nullable NSDictionary<NSString *, NSString *> *)additionalParameters;

/*! @brief Designated initializer.
@param configuration The service's configuration.
@param idTokenHint The previously issued ID Token
@param postLogoutRedirectURL The client's post-logout redirect URI.
@param state An opaque value used by the client to maintain state between the request and
callback.
@param additionalParameters The client's additional authorization parameters.
*/
- (instancetype)
initWithConfiguration:(OIDServiceConfiguration *)configuration
idTokenHint:(NSString *)idTokenHint
postLogoutRedirectURL:(NSURL *)postLogoutRedirectURL
state:(NSString *)state
additionalParameters:(nullable NSDictionary<NSString *, NSString *> *)additionalParameters
NS_DESIGNATED_INITIALIZER;

/*! @brief Constructs the request URI by adding the request parameters to the query component of the
authorization endpoint URI using the "application/x-www-form-urlencoded" format.
@return A URL representing the authorization request.
@see http://openid.net/specs/openid-connect-session-1_0.html#RPLogout
*/
- (NSURL *)endSessionRequestURL;

@end

NS_ASSUME_NONNULL_END
Loading