Skip to content

Commit

Permalink
OSX support in OKTA OIDC (#212)
Browse files Browse the repository at this point in the history
OKTA-254906 - macOS OIDC SDK: Implement authentication API similar to iOS
  • Loading branch information
IldarAbdullin-okta authored Apr 14, 2020
1 parent 5e7b611 commit 6d88e41
Show file tree
Hide file tree
Showing 149 changed files with 5,149 additions and 1,221 deletions.
12 changes: 11 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,15 @@ language: objective-c
osx_image: xcode10.3
jobs:
include:
- stage: Unit tests iOS
name: iOS
script:
- xcodebuild -project okta-oidc.xcodeproj -scheme "okta-oidc" -destination "platform=iOS Simulator,OS=latest,name=iPhone 8" clean test | xcpretty
- xcodebuild -project okta-oidc.xcodeproj -scheme "okta-oidc-ios" -destination "platform=iOS Simulator,OS=latest,name=iPhone 8" clean test
- stage: Unit tests MacOS
name: MacOS
script:
- xcodebuild -project okta-oidc.xcodeproj -scheme "okta-oidc-mac" -destination "platform=macOS" clean test
- stage: UI tests iOS
name: iOS
script:
- xcodebuild -project okta-oidc.xcodeproj -scheme "okta-oidc" -destination "platform=iOS Simulator,OS=latest,name=iPhone 8" clean test
4 changes: 3 additions & 1 deletion Okta/AppAuth/AppAuth.h
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*! @file AppAuth.h
/*! @file AppAuth.h
@brief AppAuth iOS SDK
@copyright
Copyright 2015 Google Inc. All Rights Reserved.
Expand Down 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
3 changes: 2 additions & 1 deletion Okta/AppAuth/AppAuthCore.h
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@
#import "OIDTokenResponse.h"
#import "OIDTokenUtilities.h"
#import "OIDURLSessionProvider.h"

#import "OIDEndSessionRequest.h"
#import "OIDEndSessionResponse.h"
Empty file modified Okta/AppAuth/OIDAuthState.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDAuthState.m
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDAuthStateChangeDelegate.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDAuthStateErrorDelegate.h
100755 → 100644
Empty file.
2 changes: 1 addition & 1 deletion Okta/AppAuth/OIDAuthorizationRequest.h
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ extern NSString *const OIDOAuthorizationRequestCodeChallengeMethodS256;

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

Expand Down
Empty file modified Okta/AppAuth/OIDAuthorizationRequest.m
100755 → 100644
Empty file.
2 changes: 1 addition & 1 deletion Okta/AppAuth/OIDAuthorizationResponse.h
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ NS_ASSUME_NONNULL_BEGIN
NSDictionary<NSString *, NSObject<NSCopying> *> *additionalParameters;

/*! @internal
@brief Unavailable. Please use initWithParameters:.
@brief Unavailable. Please use initWithRequest:parameters:.
*/
- (instancetype)init NS_UNAVAILABLE;

Expand Down
Empty file modified Okta/AppAuth/OIDAuthorizationResponse.m
100755 → 100644
Empty file.
23 changes: 23 additions & 0 deletions Okta/AppAuth/OIDAuthorizationService.h
100755 → 100644
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
185 changes: 170 additions & 15 deletions Okta/AppAuth/OIDAuthorizationService.m
100755 → 100644
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 All @@ -39,6 +41,10 @@
*/
static NSString *const kOpenIDConfigurationWellKnownPath = @".well-known/openid-configuration";

/*! @brief Max allowable iat (Issued At) time skew
@see https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
*/
static int const kOIDAuthorizationSessionIATMaxSkew = 600;

NS_ASSUME_NONNULL_BEGIN

Expand Down Expand Up @@ -80,24 +86,38 @@ - (void)presentAuthorizationWithExternalUserAgent:(id<OIDExternalUserAgent>)exte
}

- (void)cancel {
[self cancelWithCompletion:nil];
}

- (void)cancelWithCompletion:(nullable void (^)(void))completion {
[_externalUserAgent dismissExternalUserAgentAnimated:YES completion:^{
NSError *error = [OIDErrorUtilities errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow
underlyingError:nil
description:@"Authorization flow was cancelled."];
[self didFinishWithResponse:nil error:error];
if (completion) completion();
}];
}

- (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 redirectionURL The second redirect URI to compare.
@return YES if the URLs match down to the path level (query params are ignored).
*/
+ (BOOL)URL:(NSURL *)URL matchesRedirectionURL:(NSURL *)redirectionURL {
NSURL *standardizedURL = [URL standardizedURL];
NSURL *standardizedRedirectURL = [_request.redirectURL standardizedURL];

return OIDIsEqualIncludingNil(standardizedURL.scheme, standardizedRedirectURL.scheme) &&
OIDIsEqualIncludingNil(standardizedURL.user, standardizedRedirectURL.user) &&
OIDIsEqualIncludingNil(standardizedURL.password, standardizedRedirectURL.password) &&
OIDIsEqualIncludingNil(standardizedURL.host, standardizedRedirectURL.host) &&
OIDIsEqualIncludingNil(standardizedURL.port, standardizedRedirectURL.port) &&
OIDIsEqualIncludingNil(standardizedURL.path, standardizedRedirectURL.path);
NSURL *standardizedRedirectURL = [redirectionURL standardizedURL];

return [standardizedURL.scheme caseInsensitiveCompare:standardizedRedirectURL.scheme] == NSOrderedSame
&& OIDIsEqualIncludingNil(standardizedURL.user, standardizedRedirectURL.user)
&& OIDIsEqualIncludingNil(standardizedURL.password, standardizedRedirectURL.password)
&& OIDIsEqualIncludingNil(standardizedURL.host, standardizedRedirectURL.host)
&& OIDIsEqualIncludingNil(standardizedURL.port, standardizedRedirectURL.port)
&& OIDIsEqualIncludingNil(standardizedURL.path, standardizedRedirectURL.path);
}

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

- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)URL {
Expand Down Expand Up @@ -174,6 +194,126 @@ - (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
underlyingError:nil
description:@"Unable to open Safari."];
[self didFinishWithResponse:nil error:safariError];
}
}

- (void)cancel {
[self cancelWithCompletion:nil];
}

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

- (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
matchesRedirectionURL:_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 @@ -268,6 +408,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 Expand Up @@ -424,10 +574,12 @@ + (void)performTokenRequest:(OIDTokenRequest *)request
return;
}

// OpenID Connect Core Section 3.1.3.7. rule #3
// Validates that the audience of the ID Token matches the client ID.
// OpenID Connect Core Section 3.1.3.7. rule #3 & Section 2 azp Claim
// Validates that the aud (audience) Claim contains the client ID, or that the azp
// (authorized party) Claim matches the client ID.
NSString *clientID = tokenResponse.request.clientID;
if (![idToken.audience containsObject:clientID]) {
if (![idToken.audience containsObject:clientID] &&
![idToken.claims[@"azp"] isEqualToString:clientID]) {
NSError *invalidIDToken =
[OIDErrorUtilities errorWithCode:OIDErrorCodeIDTokenFailedValidationError
underlyingError:nil
Expand Down Expand Up @@ -467,12 +619,15 @@ + (void)performTokenRequest:(OIDTokenRequest *)request
// OpenID Connect Core Section 3.1.3.7. rule #10
// Validates that the issued at time is not more than +/- 10 minutes on the current time.
NSTimeInterval issuedAtDifference = [idToken.issuedAt timeIntervalSinceNow];
if (fabs(issuedAtDifference) > 600) {
if (fabs(issuedAtDifference) > kOIDAuthorizationSessionIATMaxSkew) {
NSString *message =
[NSString stringWithFormat:@"Issued at time is more than %d seconds before or after "
"the current time",
kOIDAuthorizationSessionIATMaxSkew];
NSError *invalidIDToken =
[OIDErrorUtilities errorWithCode:OIDErrorCodeIDTokenFailedValidationError
underlyingError:nil
description:@"Issued at time is more than 5 minutes before or after "
"the current time"];
description:message];
dispatch_async(dispatch_get_main_queue(), ^{
callback(nil, invalidIDToken);
});
Expand Down
Empty file modified Okta/AppAuth/OIDClientMetadataParameters.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDClientMetadataParameters.m
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDDefines.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDError.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDError.m
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDErrorUtilities.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDErrorUtilities.m
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDExternalUserAgent.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDExternalUserAgentRequest.h
100755 → 100644
Empty file.
13 changes: 13 additions & 0 deletions Okta/AppAuth/OIDExternalUserAgentSession.h
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
limitations under the License.
*/

NS_ASSUME_NONNULL_BEGIN

/*! @brief Represents an in-flight external user-agent session.
*/
Expand All @@ -30,6 +31,16 @@
*/
- (void)cancel;

/*! @brief Cancels the code flow session, invoking the request's callback with a cancelled error.
@remarks Has no effect if called more than once, or after a
@c OIDExternalUserAgentSession.resumeExternalUserAgentFlowWithURL: message was received.
Will cause an error with code: @c ::OIDErrorCodeProgramCanceledAuthorizationFlow to be
passed to the @c callback block passed to
@c OIDAuthorizationService.presentAuthorizationRequest:presentingViewController:callback:
@param completion The block to be called when the cancel operation ends
*/
- (void)cancelWithCompletion:(nullable void (^)(void))completion;

/*! @brief Clients should call this method with the result of the external user-agent code flow if
it becomes available.
@param URL The redirect URL invoked by the server.
Expand All @@ -50,3 +61,5 @@
- (void)failExternalUserAgentFlowWithError:(NSError *)error;

@end

NS_ASSUME_NONNULL_END
Empty file modified Okta/AppAuth/OIDFieldMapping.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDFieldMapping.m
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDGrantTypes.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDGrantTypes.m
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDIDToken.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDIDToken.m
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDRegistrationRequest.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDRegistrationRequest.m
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDRegistrationResponse.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDRegistrationResponse.m
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDResponseTypes.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDResponseTypes.m
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDScopeUtilities.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDScopeUtilities.m
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDScopes.h
100755 → 100644
Empty file.
Empty file modified Okta/AppAuth/OIDScopes.m
100755 → 100644
Empty file.
7 changes: 7 additions & 0 deletions Okta/AppAuth/OIDServiceDiscovery.m
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ - (nullable instancetype)initWithJSONData:(NSData *)serviceDiscoveryJSONData
description:jsonError.localizedDescription];
return nil;
}
if (![json isKindOfClass:[NSDictionary class]]) {
*error = [OIDErrorUtilities errorWithCode:OIDErrorCodeInvalidDiscoveryDocument
underlyingError:nil
description:@"Discovery document isn't a dictionary"];
return nil;
}

return [self initWithDictionary:json error:error];
}

Expand Down
Loading

0 comments on commit 6d88e41

Please sign in to comment.