diff --git a/Source/AppAuth.h b/Source/AppAuth.h index 908c6f30c..c01c02c3e 100644 --- a/Source/AppAuth.h +++ b/Source/AppAuth.h @@ -26,6 +26,7 @@ #import "OIDError.h" #import "OIDErrorUtilities.h" #import "OIDGrantTypes.h" +#import "OIDIDToken.h" #import "OIDRegistrationRequest.h" #import "OIDRegistrationResponse.h" #import "OIDResponseTypes.h" diff --git a/Source/Framework/AppAuth.h b/Source/Framework/AppAuth.h index 1275e36f2..7c1ea78e5 100644 --- a/Source/Framework/AppAuth.h +++ b/Source/Framework/AppAuth.h @@ -34,6 +34,7 @@ FOUNDATION_EXPORT const unsigned char AppAuthVersionString[]; #import #import #import +#import #import #import #import diff --git a/Source/OIDAuthorizationService.m b/Source/OIDAuthorizationService.m index e1179c76c..0b703e17f 100644 --- a/Source/OIDAuthorizationService.m +++ b/Source/OIDAuthorizationService.m @@ -23,6 +23,7 @@ #import "OIDAuthorizationUICoordinator.h" #import "OIDDefines.h" #import "OIDErrorUtilities.h" +#import "OIDIDToken.h" #import "OIDRegistrationRequest.h" #import "OIDRegistrationResponse.h" #import "OIDServiceConfiguration.h" @@ -344,6 +345,57 @@ + (void)performTokenRequest:(OIDTokenRequest *)request callback:(OIDTokenCallbac return; } + // Validates ID Token if it exists + if (tokenResponse.idToken) { + OIDIDToken *idToken = [[OIDIDToken alloc] initWithIDTokenString:tokenResponse.idToken]; + if (!idToken) { + NSError *invalidIDToken = + [OIDErrorUtilities errorWithCode:OIDErrorCodeIDTokenInvalidToken + underlyingError:nil + description:@"ID Token parsing failed"]; + dispatch_async(dispatch_get_main_queue(), ^{ + callback(nil, invalidIDToken); + }); + return; + } + + NSURL *issuer = tokenResponse.request.configuration.discoveryDocument.issuer; + if (issuer && ![idToken.issuer isEqual:issuer]) { + NSError *invalidIDToken = + [OIDErrorUtilities errorWithCode:OIDErrorCodeIDTokenInvalidToken + underlyingError:nil + description:@"Issuer mismatch"]; + dispatch_async(dispatch_get_main_queue(), ^{ + callback(nil, invalidIDToken); + }); + return; + } + + NSString *clientID = tokenResponse.request.clientID; + if (![idToken.audience isEqual:clientID]) { + NSError *invalidIDToken = + [OIDErrorUtilities errorWithCode:OIDErrorCodeIDTokenInvalidToken + underlyingError:nil + description:@"Audience mismatch"]; + dispatch_async(dispatch_get_main_queue(), ^{ + callback(nil, invalidIDToken); + }); + return; + } + + NSTimeInterval difference = [idToken.issuedAt timeIntervalSinceNow]; + if (fabs(difference) > 300) { + NSError *invalidIDToken = + [OIDErrorUtilities errorWithCode:OIDErrorCodeIDTokenInvalidToken + underlyingError:nil + description:@"IAT time invalid"]; + dispatch_async(dispatch_get_main_queue(), ^{ + callback(nil, invalidIDToken); + }); + return; + } + } + // Success dispatch_async(dispatch_get_main_queue(), ^{ callback(tokenResponse, nil); diff --git a/Source/OIDError.h b/Source/OIDError.h index 229802cc0..88d55d220 100644 --- a/Source/OIDError.h +++ b/Source/OIDError.h @@ -144,6 +144,13 @@ typedef NS_ENUM(NSInteger, OIDErrorCode) { */ OIDErrorCodeJSONSerializationError = -13, + /*! @brief The ID Token did not parse. + */ + OIDErrorCodeIDTokenInvalidToken = -14, + + /*! @brief The ID Token did not pass validation (e.g. issuer, audience checks). + */ + OIDErrorCodeIDTokenFailedValidation = -15, }; /*! @brief Enum of all possible OAuth error codes as defined by RFC6749 diff --git a/Source/OIDIDToken.h b/Source/OIDIDToken.h new file mode 100644 index 000000000..9d3314ecd --- /dev/null +++ b/Source/OIDIDToken.h @@ -0,0 +1,76 @@ +/*! @file OIDIDToken.h + @brief AppAuth iOS SDK + @copyright + Copyright 2017 Google Inc. 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 + +NS_ASSUME_NONNULL_BEGIN + +/*! @brief A convenience class that parses _but not validates_ and ID Token. + */ +@interface OIDIDToken : NSObject + +/*! @internal + @brief Unavailable. Please use @c initWithAuthorizationResponse:. + */ +- (instancetype)init NS_UNAVAILABLE; + +/*! @brief Parses the given ID Token string. + @param idToken The ID Token spring. + */ +- (nullable instancetype)initWithIDTokenString:(NSString *)idToken; + +/*! @brief The header JWT values. + */ +@property(nonatomic, readonly) NSDictionary *header; + +/*! @brief All ID Token claims. + */ +@property(nonatomic, readonly) NSDictionary *claims; + +/*! @brief Issuer Identifier for the Issuer of the response. + @remarks iss + @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken + */ +@property(nonatomic, readonly) NSURL *issuer; + +/*! @brief Subject Identifier. + @remarks sub + @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken + */ +@property(nonatomic, readonly) NSString *subject; + +/*! @brief Audience(s) that this ID Token is intended for. + @remarks aud + @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken + */ +@property(nonatomic, readonly) NSString *audience; + +/*! @brief Expiration time on or after which the ID Token MUST NOT be accepted for processing. + @remarks exp + @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken + */ +@property(nonatomic, readonly) NSDate *expiresAt; + +/*! @brief Time at which the JWT was issued. + @remarks iat + @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken + */ +@property(nonatomic, readonly) NSDate *issuedAt; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/OIDIDToken.m b/Source/OIDIDToken.m new file mode 100644 index 000000000..8f767ff1c --- /dev/null +++ b/Source/OIDIDToken.m @@ -0,0 +1,134 @@ +/*! @file OIDIDToken.m + @brief AppAuth iOS SDK + @copyright + Copyright 2017 Google Inc. 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 "OIDIDToken.h" + +/*! Field keys associated with an ID Token. */ +static NSString *const kIssKey = @"iss"; +static NSString *const kSubKey = @"sub"; +static NSString *const kAudKey = @"aud"; +static NSString *const kExpKey = @"exp"; +static NSString *const kIatKey = @"iat"; + +#import "OIDFieldMapping.h" + +@implementation OIDIDToken { +} + +- (instancetype)initWithIDTokenString:(NSString *)idToken { + self = [super init]; + NSArray *sections = [idToken componentsSeparatedByString:@"."]; + if (sections.count > 1) { + _header = [[self class] parseJWTSection:sections[0]]; + _claims = [[self class] parseJWTSection:sections[1]]; + if (!_header || !_claims) { + return nil; + } + + [OIDFieldMapping remainingParametersWithMap:[[self class] fieldMap] + parameters:_claims + instance:self]; + + // Required fields. + if (!_issuer || !_audience || !_subject || !_expiresAt || !_issuedAt) { + return nil; + } + + return self; + } + return nil; +} + +/*! @brief Returns a mapping of incoming parameters to instance variables. + @return A mapping of incoming parameters to instance variables. + */ ++ (NSDictionary *)fieldMap { + static NSMutableDictionary *fieldMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + fieldMap = [NSMutableDictionary dictionary]; + + fieldMap[kIssKey] = + [[OIDFieldMapping alloc] initWithName:@"_issuer" + type:[NSURL class] + conversion:[OIDFieldMapping URLConversion]]; + fieldMap[kSubKey] = + [[OIDFieldMapping alloc] initWithName:@"_subject" type:[NSString class]]; + fieldMap[kAudKey] = + [[OIDFieldMapping alloc] initWithName:@"_audience" type:[NSString class]]; + fieldMap[kExpKey] = + [[OIDFieldMapping alloc] initWithName:@"_expiresAt" + type:[NSDate class] + conversion:^id _Nullable(NSObject *_Nullable value) { + if (![value isKindOfClass:[NSNumber class]]) { + return value; + } + NSNumber *valueAsNumber = (NSNumber *)value; + return [NSDate dateWithTimeIntervalSince1970:[valueAsNumber longLongValue]]; + }]; + fieldMap[kIatKey] = + [[OIDFieldMapping alloc] initWithName:@"_issuedAt" + type:[NSDate class] + conversion:^id _Nullable(NSObject *_Nullable value) { + if (![value isKindOfClass:[NSNumber class]]) { + return value; + } + NSNumber *valueAsNumber = (NSNumber *)value; + return [NSDate dateWithTimeIntervalSince1970:[valueAsNumber longLongValue]]; + }]; + }); + return fieldMap; +} + ++ (NSDictionary *)parseJWTSection:(NSString *)sectionString { + NSData *decodedData = [[self class] base64urlNoPaddingDecode:sectionString]; + + // Parses JSON. + NSError *error; + id object = [NSJSONSerialization JSONObjectWithData:decodedData options:0 error:&error]; + if (error) { + NSLog(@"Error %@ parsing token payload %@", error, sectionString); + } + if ([object isKindOfClass:[NSDictionary class]]) { + return (NSDictionary *)object; + } + + return nil; +} + ++ (NSData *)base64urlNoPaddingDecode:(NSString *)base64urlNoPaddingString { + NSMutableString *body = [base64urlNoPaddingString mutableCopy]; + + // Converts base64url to base64. + NSRange range = NSMakeRange(0, base64urlNoPaddingString.length); + [body replaceOccurrencesOfString:@"-" withString:@"+" options:NSLiteralSearch range:range]; + [body replaceOccurrencesOfString:@"_" withString:@"/" options:NSLiteralSearch range:range]; + + // Converts base64 no padding to base64 with padding + while (body.length % 4 != 0) { + [body appendString:@"="]; + } + + // Decodes base64 string. + NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:body options:0]; + return decodedData; +} + +@end + +