Просмотр исходного кода

TokenClaimsInternalOptions Implementation + Unit Tests

Akshat Gandhi 7 месяцев назад
Родитель
Сommit
16ed87141e

+ 43 - 0
GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h

@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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>
+
+@class GIDTokenClaim;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An internal utility class for processing and serializing the NSSet of GIDTokenClaim objects
+ * into the JSON format required for an OIDAuthorizationRequest.
+ */
+@interface GIDTokenClaimsInternalOptions : NSObject
+
+/**
+ * Processes the NSSet of GIDTokenClaim objects, handling ambiguous claims, and returns a JSON string.
+ *
+ * @param claims The NSSet of GIDTokenClaim objects provided by the developer.
+ * @param error A pointer to an NSError object to be populated if an error occurs (e.g., if a
+ * claim is requested as both essential and non-essential).
+ * @return A JSON string representing the claims request, or nil if the input is empty or an
+ * error occurs.
+ */
++ (nullable NSString *)validatedJSONStringForClaims:(nullable NSSet<GIDTokenClaim *> *)claims
+                                              error:(NSError **)error;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 71 - 0
GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.m

@@ -0,0 +1,71 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 "GIDTokenClaimsInternalOptions.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+
+@implementation GIDTokenClaimsInternalOptions
+
++ (nullable NSString *)validatedJSONStringForClaims:(nullable NSSet<GIDTokenClaim *> *)claims
+                                              error:(NSError **)error {
+  if (!claims || claims.count == 0) {
+    return nil;
+  }
+
+  // === Step 1: Check for claims with ambiguous essential property. ===
+  NSMutableDictionary<NSString *, GIDTokenClaim *> *validTokenClaims =
+  [[NSMutableDictionary alloc] init];
+
+  for (GIDTokenClaim *currentClaim in claims) {
+    GIDTokenClaim *existingClaim = validTokenClaims[currentClaim.name];
+
+    // Check for a conflict: a claim with the same name but different essentiality.
+    if (existingClaim && existingClaim.isEssential != currentClaim.isEssential) {
+      if (error) {
+        *error = [NSError errorWithDomain:kGIDSignInErrorDomain
+                                     code:kGIDSignInErrorCodeAmbiguousClaims
+                                 userInfo:@{NSLocalizedDescriptionKey: @"The claim was requested as both essential and non-essential. Please provide only one version."}];
+      }
+      return nil; // Validation failed
+    }
+    validTokenClaims[currentClaim.name] = currentClaim;
+  }
+
+  // === Step 2: Build the dictionary structure required for OIDC JSON ===
+  NSMutableDictionary<NSString *, id> *tokenClaimsDictionary = [[NSMutableDictionary alloc] init];
+  for (GIDTokenClaim *claim in validTokenClaims.allValues) {
+    if (claim.isEssential) {
+      tokenClaimsDictionary[claim.name] = @{ @"essential": @YES };
+    } else {
+      // Per OIDC spec, non-essential claims can be represented by null.
+      tokenClaimsDictionary[claim.name] = [NSNull null];
+    }
+  }
+  NSDictionary<NSString *, id> *finalRequestDictionary = @{ @"id_token": tokenClaimsDictionary };
+
+  // === Step 3: Serialize the final dictionary into a JSON string ===
+  NSData *jsonData = [NSJSONSerialization dataWithJSONObject:finalRequestDictionary
+                                                     options:0
+                                                       error:error];
+  if (!jsonData) {
+    return nil;
+  }
+
+  return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
+}
+
+@end

+ 2 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h

@@ -45,6 +45,8 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) {
   kGIDSignInErrorCodeCanceled = -5,
   /// Indicates an Enterprise Mobility Management related error has occurred.
   kGIDSignInErrorCodeEMM = -6,
+  /// Indicates a claim was requested as both essential and non-essential .
+  kGIDSignInErrorCodeAmbiguousClaims = -7,
   /// Indicates the requested scopes have already been granted to the `currentUser`.
   kGIDSignInErrorCodeScopesAlreadyGranted = -8,
   /// Indicates there is an operation on a previous user.

+ 89 - 0
GoogleSignIn/Tests/Unit/GIDTokenClaimsInternalOptionsTest.m

@@ -0,0 +1,89 @@
+#import <XCTest/XCTest.h>
+#import "GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h"
+
+@import OCMock;
+
+
+@interface GIDTokenClaimsInternalOptionsTest : XCTestCase
+@end
+
+@implementation GIDTokenClaimsInternalOptionsTest
+
+#pragma mark - Input Validation Tests
+
+- (void)testValidatedJSONStringForClaims_withNilInput_shouldReturnNil {
+  XCTAssertNil([GIDTokenClaimsInternalOptions validatedJSONStringForClaims:nil error:nil]);
+}
+
+- (void)testValidatedJSONStringForClaims_withEmptyInput_shouldReturnNil {
+  XCTAssertNil([GIDTokenClaimsInternalOptions validatedJSONStringForClaims:[NSSet set] error:nil]);
+}
+
+#pragma mark - Correct Formatting Tests
+
+- (void)testValidatedJSONStringForClaims_withSingleNonEssentialClaim_isCorrectlyFormatted {
+  NSSet *claims = [NSSet setWithObject:[GIDTokenClaim authTimeClaim]];
+  NSString *expectedJSON = @"{\"id_token\":{\"auth_time\":null}}";
+
+  NSError *error = nil;
+  NSString *result = [GIDTokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error];
+
+  XCTAssertNil(error);
+  XCTAssertEqualObjects(result, expectedJSON);
+}
+
+- (void)testValidatedJSONStringForClaims_withSingleEssentialClaim_isCorrectlyFormatted {
+  NSSet *claims = [NSSet setWithObject:[GIDTokenClaim essentialAuthTimeClaim]];
+  NSString *expectedJSON = @"{\"id_token\":{\"auth_time\":{\"essential\":true}}}";
+
+  NSError *error = nil;
+  NSString *result = [GIDTokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error];
+
+  XCTAssertNil(error);
+  XCTAssertEqualObjects(result, expectedJSON);
+}
+
+#pragma mark - Client Error Handling Tests
+
+- (void)testValidatedJSONStringForClaims_withConflictingClaims_returnsNilAndPopulatesError {
+  // Arrange
+  NSSet *claims = [NSSet setWithObjects:[GIDTokenClaim authTimeClaim],
+                                        [GIDTokenClaim essentialAuthTimeClaim],
+                                        nil];
+  NSError *error = nil;
+
+  // Act
+  NSString *result = [GIDTokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error];
+
+  // Assert
+  XCTAssertNil(result, @"Method should return nil for conflicting claims.");
+  XCTAssertNotNil(error, @"An error object should be populated.");
+  XCTAssertEqualObjects(error.domain, kGIDSignInErrorDomain, @"Error domain should be correct.");
+  XCTAssertEqual(error.code, kGIDSignInErrorCodeAmbiguousClaims, @"Error code should be for ambiguous claims.");
+}
+
+- (void)testValidatedJSONStringForClaims_whenSerializationFails_returnsNilAndError {
+  // 1. Arrange
+  NSSet *claims = [NSSet setWithObject:[GIDTokenClaim authTimeClaim]];
+  NSError *fakeJSONError = [NSError errorWithDomain:@"com.fake.json" code:-999 userInfo:nil];
+  id mockSerialization = OCMClassMock([NSJSONSerialization class]);
+
+  OCMStub([mockSerialization dataWithJSONObject:OCMOCK_ANY
+                                         options:0
+                                           error:[OCMArg setTo:fakeJSONError]]).andReturn(nil);
+
+  // 2. Act
+  NSError *actualError = nil;
+  NSString *result =
+      [GIDTokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&actualError];
+
+  // 3. Assert
+  XCTAssertNil(result, @"The result should be nil when JSON serialization fails.");
+  XCTAssertEqualObjects(actualError, fakeJSONError,
+                        @"The error from serialization should be passed back to the caller.");
+
+  [mockSerialization stopMocking];
+}
+
+@end