Skip to content

Commit

Permalink
Feature: custom token validator (#334)
Browse files Browse the repository at this point in the history
* Adds ability to use a custom token validator to verify issueAt and expiry times and updates the Example application to use TrueTime as a sample

* Implement fixes to support token validation functionality

* Remove cocoapods deps

* Add token type to validate access and id tokens

* Fix failure

* Fix linkage issue

* Remove useless comments

Co-authored-by: Erik Manor <[email protected]>
  • Loading branch information
oleggnidets-okta and emanor-okta authored Feb 3, 2022
1 parent d090c92 commit e5450a3
Show file tree
Hide file tree
Showing 34 changed files with 388 additions and 88 deletions.
68 changes: 57 additions & 11 deletions Example/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@ final class ViewController: UIViewController {

let configuration = try? OktaOidcConfig.default()
configuration?.requestCustomizationDelegate = self

configuration?.tokenValidator = self

oktaAppAuth = try? OktaOidc(configuration: isUITest ? testConfig : configuration)
AppDelegate.shared.oktaOidc = oktaAppAuth

if let config = oktaAppAuth?.configuration {
authStateManager = OktaOidcStateManager.readFromSecureStorage(for: config)
authStateManager?.requestCustomizationDelegate = self
Expand Down Expand Up @@ -117,21 +119,43 @@ final class ViewController: UIViewController {

@IBAction func introspectButton(_ sender: Any) {
// Get current accessToken
guard let accessToken = authStateManager?.accessToken else { return }

authStateManager?.introspect(token: accessToken, callback: { payload, error in
guard let isValid = payload?["active"] as? Bool else {
self.showMessage("Error: \(error?.localizedDescription ?? "Unknown")")
return
var accessToken = authStateManager?.accessToken
if accessToken == nil {
authStateManager?.renew { newAuthStateManager, error in
if let error = error {
// Error
print("Error trying to Refresh AccessToken: \(error)")
return
}
self.authStateManager = newAuthStateManager
accessToken = newAuthStateManager?.accessToken

self.authStateManager?.introspect(token: accessToken, callback: { payload, error in
guard let isValid = payload?["active"] as? Bool else {
self.showMessage("Error: \(error?.localizedDescription ?? "Unknown")")
return
}

self.showMessage("Is the AccessToken valid? - \(isValid)")
})
}

self.showMessage("Is the AccessToken valid? - \(isValid)")
})
} else {
authStateManager?.introspect(token: accessToken, callback: { payload, error in
guard let isValid = payload?["active"] as? Bool else {
self.showMessage("Error: \(error?.localizedDescription ?? "Unknown")")
return
}

self.showMessage("Is the AccessToken valid? - \(isValid)")
})
}
}

@IBAction func revokeButton(_ sender: Any) {
// Get current accessToken
guard let accessToken = authStateManager?.accessToken else { return }
guard let accessToken = authStateManager?.accessToken else {
return
}

authStateManager?.revoke(accessToken) { _, error in
if error != nil { self.showMessage("Error: \(error!)") }
Expand Down Expand Up @@ -224,6 +248,28 @@ extension ViewController: OktaNetworkRequestCustomizationDelegate {
}
}

extension ViewController: OKTTokenValidator {
func isIssued(atDateValid issuedAt: Date?, token: OKTTokenType) -> Bool {
guard let issuedAt = issuedAt else {
return false
}

let now = Date()

return fabs(now.timeIntervalSince(issuedAt)) <= 200
}

func isDateExpired(_ expiry: Date?, token tokenType: OKTTokenType) -> Bool {
guard let expiry = expiry else {
return false
}

let now = Date()

return now >= expiry
}
}

extension OktaOidcError {
var displayMessage: String {
switch self {
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,17 @@ if #available(iOS 13.0, *) {
```
***Note*** Flag is available on iOS 13 and above versions


### Token Time Validation

Custom token time validation is possible by adopting to `OKTTokenValidator` protocol and then setting `tokenValidator` variable:

```swift
configuration?.tokenValidator = self
```

By default `OKTDefaultTokenValidator` object is set.

### How to use in Objective-C project

To use this SDK in Objective-C project, you should do the following:
Expand Down
19 changes: 15 additions & 4 deletions Sources/AppAuth/OKTAuthState.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#import "OKTTokenRequest.h"
#import "OKTTokenResponse.h"
#import "OKTTokenUtilities.h"
#import "OKTDefaultTokenValidator.h"

/*! @brief Key used to encode the @c refreshToken property for @c NSSecureCoding.
*/
Expand Down Expand Up @@ -131,6 +132,7 @@ @implementation OKTAuthState {
authStateByPresentingAuthorizationRequest:(OKTAuthorizationRequest *)authorizationRequest
externalUserAgent:(id<OKTExternalUserAgent>)externalUserAgent
delegate:(id<OktaNetworkRequestCustomizationDelegate> _Nullable)delegate
validator:(id<OKTTokenValidator> _Nonnull)validator
callback:(OKTAuthStateAuthorizationCallback)callback {
// presents the authorization request
id<OKTExternalUserAgentSession> authFlowSession = [OKTAuthorizationService
Expand All @@ -151,6 +153,7 @@ @implementation OKTAuthState {
[OKTAuthorizationService performTokenRequest:tokenExchangeRequest
originalAuthorizationResponse:authorizationResponse
delegate:delegate
validator:validator
callback:^(OKTTokenResponse * _Nullable tokenResponse, NSError * _Nullable tokenError) {
OKTAuthState *authState;
if (tokenResponse) {
Expand Down Expand Up @@ -199,7 +202,11 @@ - (instancetype)initWithAuthorizationResponse:(OKTAuthorizationResponse *)author
*/
- (instancetype)initWithAuthorizationResponse:(OKTAuthorizationResponse *)authorizationResponse
tokenResponse:(nullable OKTTokenResponse *)tokenResponse {
return [self initWithAuthorizationResponse:authorizationResponse tokenResponse:tokenResponse registrationResponse:nil delegate:nil];
return [self initWithAuthorizationResponse:authorizationResponse
tokenResponse:tokenResponse
registrationResponse:nil
delegate:nil
validator:[OKTDefaultTokenValidator new]];
}

/*! @brief Creates an auth state from an registration response.
Expand All @@ -209,17 +216,20 @@ - (instancetype)initWithRegistrationResponse:(OKTRegistrationResponse *)registra
return [self initWithAuthorizationResponse:nil
tokenResponse:nil
registrationResponse:registrationResponse
delegate:nil];
delegate:nil
validator:[OKTDefaultTokenValidator new]];
}

- (instancetype)initWithAuthorizationResponse:
(nullable OKTAuthorizationResponse *)authorizationResponse
tokenResponse:(nullable OKTTokenResponse *)tokenResponse
registrationResponse:(nullable OKTRegistrationResponse *)registrationResponse
delegate:(nullable id<OktaNetworkRequestCustomizationDelegate>)delegate {
delegate:(nullable id<OktaNetworkRequestCustomizationDelegate>)delegate
validator:(nonnull id<OKTTokenValidator>)validator {
self = [super init];
if (self) {
_delegate = delegate;
_validator = validator;
_pendingActionsSyncObject = [[NSObject alloc] init];

if (registrationResponse) {
Expand Down Expand Up @@ -510,13 +520,14 @@ - (void)performActionWithFreshTokens:(OKTAuthStateAction)action
// creates a list of pending actions, starting with this one
_pendingActions = [NSMutableArray arrayWithObject:pendingAction];
}

// refresh the tokens
OKTTokenRequest *tokenRefreshRequest =
[self tokenRefreshRequestWithAdditionalParameters:additionalParameters];
[OKTAuthorizationService performTokenRequest:tokenRefreshRequest
originalAuthorizationResponse:_lastAuthorizationResponse
delegate:_delegate
validator:_validator
callback:^(OKTTokenResponse *_Nullable response,
NSError *_Nullable error) {
// update OKTAuthState based on response
Expand Down
50 changes: 23 additions & 27 deletions Sources/AppAuth/OKTAuthorizationService.m
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,13 @@
#import "OKTTokenResponse.h"
#import "OKTURLQueryComponent.h"
#import "OKTURLSessionProvider.h"
#import "OKTDefaultTokenValidator.h"

/*! @brief Path appended to an OpenID Connect issuer for discovery
@see https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
*/
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 kOKTAuthorizationSessionIATMaxSkew = 600;

NS_ASSUME_NONNULL_BEGIN

@interface OKTAuthorizationSession : NSObject<OKTExternalUserAgentSession>
Expand Down Expand Up @@ -425,12 +421,17 @@ + (void)discoverServiceConfigurationForDiscoveryURL:(NSURL *)discoveryURL
+ (void)performTokenRequest:(OKTTokenRequest *)request
delegate:(id<OktaNetworkRequestCustomizationDelegate> _Nullable)delegate
callback:(OKTTokenCallback)callback {
[[self class] performTokenRequest:request originalAuthorizationResponse:nil delegate:delegate callback:callback];
[[self class] performTokenRequest:request
originalAuthorizationResponse:nil
delegate:delegate
validator:[[OKTDefaultTokenValidator alloc] init]
callback:callback];
}

+ (void)performTokenRequest:(OKTTokenRequest *)request
originalAuthorizationResponse:(OKTAuthorizationResponse *_Nullable)authorizationResponse
delegate:(id<OktaNetworkRequestCustomizationDelegate> _Nullable)delegate
validator:(id<OKTTokenValidator> _Nonnull)validator
callback:(OKTTokenCallback)callback {

NSURLRequest *URLRequest = [request URLRequest];
Expand Down Expand Up @@ -607,37 +608,32 @@ + (void)performTokenRequest:(OKTTokenRequest *)request

// OpenID Connect Core Section 3.1.3.7. rules #7 & #8
// Not applicable. See rule #6.

NSAssert(validator != nil, @"Validator parameter is missed. Default will be used.");
id<OKTTokenValidator> tokenValidator = validator ?: [OKTDefaultTokenValidator new];

// OpenID Connect Core Section 3.1.3.7. rule #9
// Validates that the current time is before the expiry time.
NSTimeInterval expiresAtDifference = [idToken.expiresAt timeIntervalSinceNow];
if (expiresAtDifference < 0) {
if ([tokenValidator isDateExpired:idToken.expiresAt token:OKTTokenTypeId]) {
NSError *invalidIDToken =
[OKTErrorUtilities errorWithCode:OKTErrorCodeIDTokenFailedValidationError
underlyingError:nil
description:@"ID Token expired"];
[OKTErrorUtilities errorWithCode:OKTErrorCodeIDTokenFailedValidationError
underlyingError:nil
description:@"ID Token expired"];
dispatch_async(dispatch_get_main_queue(), ^{
callback(nil, invalidIDToken);
});
return;
}

// 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) > kOKTAuthorizationSessionIATMaxSkew) {
NSString *message =
[NSString stringWithFormat:@"Issued at time is more than %d seconds before or after "
"the current time",
kOKTAuthorizationSessionIATMaxSkew];
NSError *invalidIDToken =

if (![tokenValidator isIssuedAtDateValid:idToken.issuedAt token:OKTTokenTypeId]) {
NSString *message =
[NSString stringWithFormat:@"Issued at time is invalid corresponding to the current time"];
NSError *invalidIDToken =
[OKTErrorUtilities errorWithCode:OKTErrorCodeIDTokenFailedValidationError
underlyingError:nil
description:message];
dispatch_async(dispatch_get_main_queue(), ^{
callback(nil, invalidIDToken);
});
return;
dispatch_async(dispatch_get_main_queue(), ^{
callback(nil, invalidIDToken);
});
return;
}

// Only relevant for the authorization_code response type
Expand Down
50 changes: 50 additions & 0 deletions Sources/AppAuth/OKTDefaultTokenValidator.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2022-Present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (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 "OKTDefaultTokenValidator.h"

int const kOKTAuthorizationSessionIATMaxSkew = 600;

@implementation OKTDefaultTokenValidator

- (BOOL)isDateExpired:(nullable NSDate *)expiresAtDate token:(OKTTokenType)tokenType {
if (!expiresAtDate) {
return YES;
}

switch (tokenType) {
case OKTTokenTypeId: {
// OpenID Connect Core Section 3.1.3.7. rule #9
// Validates that the current time is before the expiry time.
NSTimeInterval expiresAtDifference = [expiresAtDate timeIntervalSinceNow];
return expiresAtDifference < 0;
}
case OKTTokenTypeAccess:
return expiresAtDate.timeIntervalSince1970 <= [NSDate new].timeIntervalSince1970;
default:
NSAssert(NO, @"Unknown token type.");
return YES;
}
}

- (BOOL)isIssuedAtDateValid:(nullable NSDate *)issuedAt token:(OKTTokenType)tokenType {
if (!issuedAt) {
return NO;
}

// 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 = [issuedAt timeIntervalSinceNow];
return fabs(issuedAtDifference) <= kOKTAuthorizationSessionIATMaxSkew;
}

@end
4 changes: 4 additions & 0 deletions Sources/AppAuth/iOS/OKTAuthState+IOS.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,28 @@ @implementation OKTAuthState (IOS)
authStateByPresentingAuthorizationRequest:(OKTAuthorizationRequest *)authorizationRequest
presentingViewController:(UIViewController *)presentingViewController
delegate:(id<OktaNetworkRequestCustomizationDelegate> _Nullable)delegate
validator:(id<OKTTokenValidator> _Nullable)validator
callback:(OKTAuthStateAuthorizationCallback)callback {
OKTExternalUserAgentIOS *externalUserAgent =
[[OKTExternalUserAgentIOS alloc]
initWithPresentingViewController:presentingViewController];
return [self authStateByPresentingAuthorizationRequest:authorizationRequest
externalUserAgent:externalUserAgent
delegate:delegate
validator: validator
callback:callback];
}

+ (id<OKTExternalUserAgentSession>)
authStateByPresentingAuthorizationRequest:(OKTAuthorizationRequest *)authorizationRequest
delegate:(id<OktaNetworkRequestCustomizationDelegate> _Nullable)delegate
validator:(id<OKTTokenValidator> _Nullable)validator
callback:(OKTAuthStateAuthorizationCallback)callback {
OKTExternalUserAgentIOS *externalUserAgent = [[OKTExternalUserAgentIOS alloc] init];
return [self authStateByPresentingAuthorizationRequest:authorizationRequest
externalUserAgent:externalUserAgent
delegate:delegate
validator: validator
callback:callback];
}

Expand Down
1 change: 1 addition & 0 deletions Sources/AppAuth/include/AppAuthCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@
#import "OKTURLSessionProvider.h"
#import "OKTEndSessionRequest.h"
#import "OKTEndSessionResponse.h"
#import "OKTDefaultTokenValidator.h"
2 changes: 2 additions & 0 deletions Sources/AppAuth/include/OKTAuthState+IOS.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ NS_ASSUME_NONNULL_BEGIN
authStateByPresentingAuthorizationRequest:(OKTAuthorizationRequest *)authorizationRequest
presentingViewController:(UIViewController *)presentingViewController
delegate:(id<OktaNetworkRequestCustomizationDelegate> _Nullable)delegate
validator:(id<OKTTokenValidator> _Nullable)validator
callback:(OKTAuthStateAuthorizationCallback)callback;

+ (id<OKTExternalUserAgentSession>)
authStateByPresentingAuthorizationRequest:(OKTAuthorizationRequest *)authorizationRequest
delegate:(id<OktaNetworkRequestCustomizationDelegate> _Nullable)delegate
validator:(id<OKTTokenValidator> _Nullable)validator
callback:(OKTAuthStateAuthorizationCallback)callback API_AVAILABLE(ios(11))
__deprecated_msg("This method will not work on iOS 13. Use "
"authStateByPresentingAuthorizationRequest:presentingViewController:callback:");
Expand Down
Loading

0 comments on commit e5450a3

Please sign in to comment.