From 47321d7aded2e4b7cf3d96dab17f683942713b63 Mon Sep 17 00:00:00 2001 From: William Denniss Date: Mon, 17 Sep 2018 13:07:11 -0700 Subject: [PATCH 1/2] Add unit tests for x-www-form-urlencoded encoding and decoding --- UnitTests/OIDURLQueryComponentTests.m | 82 +++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/UnitTests/OIDURLQueryComponentTests.m b/UnitTests/OIDURLQueryComponentTests.m index e496897a8..e8f08c042 100644 --- a/UnitTests/OIDURLQueryComponentTests.m +++ b/UnitTests/OIDURLQueryComponentTests.m @@ -80,6 +80,88 @@ - (void)testAddingParameter { kTestParameterValue, @""); } +/*! @brief Test that URI query items are decoded correctly, using application/x-www-form-urlencoded + encoding. + @see https://tools.ietf.org/html/rfc6749#section-4.1.2 + @see https://tools.ietf.org/html/rfc6749#appendix-B + */ +- (void)test_formurlencoded_decoding { + // Authorization response URL template + NSString *responseURLtemplate = @"com.example.apps.1234-tepulg5joaks7:/?state=z634l182&code=4/WQA" + "stm4iiN_0Qi-n4mEo-jL-85CvQ&scope=%@&authuser=0&session_state=ab78c20&prompt=consent#"; + + NSString *expectedDecodedScope = + @"https://www.example.com/auth/plus.me https://www.example.com/auth/userinfo.profile"; + + // Tests an encoded scope with a '+'-encoded space + { + NSString* encodedScope = + @"https://www.example.com/auth/plus.me+https://www.example.com/auth/userinfo.profile"; + NSString *authorizationResponse = [NSString stringWithFormat:responseURLtemplate,encodedScope]; + OIDURLQueryComponent *query = + [[OIDURLQueryComponent alloc] initWithURL:[NSURL URLWithString:authorizationResponse]]; + NSString* value = [query valuesForParameter:@"scope"][0]; + XCTAssertEqualObjects(value, + expectedDecodedScope, + @"Failed to decode scope with '+' delimiter"); + } + // Tests an encoded scope with a '%20'-encoded space + { + NSString* encodedScope = + @"https://www.example.com/auth/plus.me%20https://www.example.com/auth/userinfo.profile"; + NSString *authorizationResponse = [NSString stringWithFormat:responseURLtemplate,encodedScope]; + OIDURLQueryComponent *query = + [[OIDURLQueryComponent alloc] initWithURL:[NSURL URLWithString:authorizationResponse]]; + NSString* value = [query valuesForParameter:@"scope"][0]; + XCTAssertEqualObjects(value, + expectedDecodedScope, + @"Failed to decode scope with '%%20' delimiter"); + } + // Tests that the example string from RFC6749 Appendix B is decoded correctly + { + NSString* encodedScope = @"+%25%26%2B%C2%A3%E2%82%AC"; + NSString *authorizationResponse = [NSString stringWithFormat:responseURLtemplate,encodedScope]; + OIDURLQueryComponent *query = + [[OIDURLQueryComponent alloc] initWithURL:[NSURL URLWithString:authorizationResponse]]; + NSString* value = [query valuesForParameter:@"scope"][0]; + XCTAssertEqualObjects(value, + @" %&+£€", + @"Failed to decode RFC6749 Appendix B sample string correctly."); + } +} + +/*! @brief Test that URI query items are encoded correctly, using application/x-www-form-urlencoded + encoding. Note that AppAuth always encodes "+" as "%20" (as permitted) to reduce + ambiguity. + @see https://tools.ietf.org/html/rfc6749#section-4.1.3 + @see https://tools.ietf.org/html/rfc6749#appendix-B + */ +- (void)test_formurlencoded_encoding { + NSURL *baseURL = [NSURL URLWithString:kTestURLRoot]; + // Tests that space is encoded as %20 + { + OIDURLQueryComponent *query = [[OIDURLQueryComponent alloc] initWithURL:baseURL]; + [query addParameter:@"scope" value:@"openid profile"]; + NSString *encodedParams = [query URLEncodedParameters]; + NSString *expected = @"scope=openid%20profile"; + XCTAssertEqualObjects(encodedParams, + expected, + @"Failed to encode space as %%20."); + } + // Tests that the example string from RFC6749 Appendix B is encoded correctly (but with space + // encoded as %20, not +, as allowed by application/x-www-form-urlencoded. + { + OIDURLQueryComponent *query = [[OIDURLQueryComponent alloc] initWithURL:baseURL]; + [query addParameter:@"scope" value:@" %&+£€"]; + // Tests the URLEncodedParameters method + NSString *encodedParams = [query URLEncodedParameters]; + NSString *expected = @"scope=%20%25%26%2B%C2%A3%E2%82%AC"; + XCTAssertEqualObjects(encodedParams, + expected, + @"Failed to encode RFC6749 Appendix B sample string correctly."); + } +} + - (void)testAddingTwoParameters { OIDURLQueryComponent *query = [[OIDURLQueryComponent alloc] init]; [query addParameter:kTestParameterName value:kTestParameterValue]; From aa069cd2152b2f675936ab1cc5ef9a88f3634a2b Mon Sep 17 00:00:00 2001 From: William Denniss Date: Mon, 17 Sep 2018 13:37:57 -0700 Subject: [PATCH 2/2] Implement the application/x-www-form-urlencoded format for URL query components Previously only percent encoding was used, which misses space being encoded as '+'. We're still percent encoding space during encoding (as this appears to still be valid application/x-www-form-urlencoded encoded data, since percent encoding is permitted), but it will now correctly decode '+' as space. --- Source/OIDURLQueryComponent.h | 10 +++++++++- Source/OIDURLQueryComponent.m | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Source/OIDURLQueryComponent.h b/Source/OIDURLQueryComponent.h index 2a87d4f81..b01dfff14 100644 --- a/Source/OIDURLQueryComponent.h +++ b/Source/OIDURLQueryComponent.h @@ -28,7 +28,15 @@ NS_ASSUME_NONNULL_BEGIN */ extern BOOL gOIDURLQueryComponentForceIOS7Handling; -/*! @brief A utility class for creating and parsing URL query components. +/*! @brief A utility class for creating and parsing URL query components encoded with the + application/x-www-form-urlencoded format. + @description Supports application/x-www-form-urlencoded encoding and decoding, specifically + '+' is replaced with space before percent decoding. For encoding, simply percent encodes + space, as this is valid application/x-www-form-urlencoded. + @see https://tools.ietf.org/html/rfc6749#section-4.1.2 + @see https://tools.ietf.org/html/rfc6749#section-4.1.3 + @see https://tools.ietf.org/html/rfc6749#appendix-B + @see https://url.spec.whatwg.org/#urlencoded-parsing */ @interface OIDURLQueryComponent : NSObject { // private variables diff --git a/Source/OIDURLQueryComponent.m b/Source/OIDURLQueryComponent.m index 97fb1e295..2421be4ed 100644 --- a/Source/OIDURLQueryComponent.m +++ b/Source/OIDURLQueryComponent.m @@ -44,6 +44,12 @@ - (nullable instancetype)initWithURL:(NSURL *)URL { if (!gOIDURLQueryComponentForceIOS7Handling) { NSURLComponents *components = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO]; + // As OAuth uses application/x-www-form-urlencoded encoding, interprets '+' as a space + // in addition to regular percent decoding. https://url.spec.whatwg.org/#urlencoded-parsing + components.percentEncodedQuery = + [components.percentEncodedQuery stringByReplacingOccurrencesOfString:@"+" + withString:@"%20"]; + // NB. @c queryItems are already percent decoded NSArray *queryItems = components.queryItems; for (NSURLQueryItem *queryItem in queryItems) { [self addParameter:queryItem.name value:queryItem.value]; @@ -54,6 +60,10 @@ - (nullable instancetype)initWithURL:(NSURL *)URL { // Fallback for iOS 7 NSString *query = URL.query; + // As OAuth uses application/x-www-form-urlencoded encoding, interprets '+' as a space + // in addition to regular percent decoding. https://url.spec.whatwg.org/#urlencoded-parsing + query = [query stringByReplacingOccurrencesOfString:@"+" withString:@"%20"]; + NSArray *queryParts = [query componentsSeparatedByString:@"&"]; for (NSString *queryPart in queryParts) { NSRange equalsRange = [queryPart rangeOfString:@"="];