Ver Fonte

GIDTokenClaimsInternalOptions Implementation + Unit Tests #550 (#552)

This pull request introduces the `GIDTokenClaimsInternalOptions` class, a new component designed to handle the validation and JSON serialization of token claims.

Key changes:

* Adds the `GIDTokenClaimsInternalOptions` class to validate the token claims and return a JSON object.
* Adds the `GIDJSONSerializer` protocol with real and fake implementations to support serializing the token claims.
* Provides unit tests to validate the implementation.
AkshatGandhi há 7 meses atrás
pai
commit
51868b34bf

+ 38 - 0
GoogleSignIn/Sources/GIDJSONSerializer/API/GIDJSONSerializer.h

@@ -0,0 +1,38 @@
+/*
+ * 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A protocol for serializing an `NSDictionary` into a JSON string.
+ */
+@protocol GIDJSONSerializer <NSObject>
+
+/**
+ * Serializes the given dictionary into a `JSON` string.
+ *
+ * @param jsonObject The dictionary to be serialized.
+ * @param error A pointer to an `NSError` object to be populated upon failure.
+ * @return A `JSON` string representation of the dictionary, or `nil` if an error occurs.
+ */
+- (nullable NSString *)stringWithJSONObject:(NSDictionary<NSString *, id> *)jsonObject
+                                      error:(NSError *_Nullable *_Nullable)error;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 37 - 0
GoogleSignIn/Sources/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.h

@@ -0,0 +1,37 @@
+/*
+ * 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 "GoogleSignIn/Sources/GIDJSONSerializer/API/GIDJSONSerializer.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** A fake implementation of `GIDJSONSerializer` for testing purposes. */
+@interface GIDFakeJSONSerializerImpl : NSObject <GIDJSONSerializer>
+
+/**
+ * An error to be returned by `stringWithJSONObject:error:`.
+ *
+ * If this property is set, `stringWithJSONObject:error:` will return `nil` and
+ * populate the error parameter with this error.
+ */
+@property(nonatomic, nullable) NSError *serializationError;
+
+/** The dictionary passed to the serialization method. */
+@property(nonatomic, readonly, nullable) NSDictionary<NSString *, id> *capturedJSONObject;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 46 - 0
GoogleSignIn/Sources/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.m

@@ -0,0 +1,46 @@
+/*
+ * 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 "GoogleSignIn/Sources/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.h"
+
+#import "GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+
+@implementation GIDFakeJSONSerializerImpl
+
+- (nullable NSString *)stringWithJSONObject:(NSDictionary<NSString *, id> *)jsonObject
+                                      error:(NSError *_Nullable *_Nullable)error {
+  _capturedJSONObject = [jsonObject copy];
+
+  // Check if a serialization error should be simulated.
+  if (self.serializationError) {
+    if (error) {
+      *error = self.serializationError;
+    }
+    return nil;
+  }
+
+  // If not failing, fall back to the real serialization path.
+  NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObject
+                                                 options:0
+                                                   error:error];
+  if (!jsonData) {
+      return nil;
+  }
+  return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
+}
+
+@end

+ 26 - 0
GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h

@@ -0,0 +1,26 @@
+/*
+ * 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 "GoogleSignIn/Sources/GIDJSONSerializer/API/GIDJSONSerializer.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+extern NSString *const kGIDJSONSerializationErrorDescription;
+
+@interface GIDJSONSerializerImpl : NSObject <GIDJSONSerializer>
+@end
+
+NS_ASSUME_NONNULL_END

+ 46 - 0
GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.m

@@ -0,0 +1,46 @@
+/*
+ * 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 "GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h"
+
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+
+NSString * const kGIDJSONSerializationErrorDescription =
+    @"The provided object could not be serialized to a JSON string.";
+
+@implementation GIDJSONSerializerImpl
+
+- (nullable NSString *)stringWithJSONObject:(NSDictionary<NSString *, id> *)jsonObject
+                                      error:(NSError *_Nullable *_Nullable)error {
+  NSError *serializationError;
+  NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObject
+                                                     options:0
+                                                       error:&serializationError];
+  if (!jsonData) {
+    if (error) {
+      *error = [NSError errorWithDomain:kGIDSignInErrorDomain
+                                   code:kGIDSignInErrorCodeJSONSerializationFailure
+                               userInfo:@{
+                            NSLocalizedDescriptionKey:kGIDJSONSerializationErrorDescription,
+                                 NSUnderlyingErrorKey:serializationError
+                               }];
+    }
+    return nil;
+  }
+  return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
+}
+
+@end

+ 55 - 0
GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h

@@ -0,0 +1,55 @@
+/*
+ * 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
+
+extern NSString *const kGIDTokenClaimErrorDescription;
+extern NSString *const kGIDTokenClaimEssentialPropertyKeyName;
+extern NSString *const kGIDTokenClaimKeyName;
+
+@protocol GIDJSONSerializer;
+
+/**
+ * An internal utility class for processing and serializing the `NSSet` of `GIDTokenClaim` objects
+ * into the `JSON` format required for an `OIDAuthorizationRequest`.
+ */
+@interface GIDTokenClaimsInternalOptions : NSObject
+
+- (instancetype)init;
+
+- (instancetype)initWithJSONSerializer:
+    (id<GIDJSONSerializer>)jsonSerializer NS_DESIGNATED_INITIALIZER;
+
+/**
+ * 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 *_Nullable *_Nullable)error;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 91 - 0
GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.m

@@ -0,0 +1,91 @@
+/*
+ * 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 "GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h"
+
+#import "GoogleSignIn/Sources/GIDJSONSerializer/API/GIDJSONSerializer.h"
+#import "GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h"
+
+NSString * const kGIDTokenClaimErrorDescription =
+    @"The claim was requested as both essential and non-essential. "
+    @"Please provide only one version.";
+NSString * const kGIDTokenClaimEssentialPropertyKey = @"essential";
+NSString * const kGIDTokenClaimKeyName = @"id_token";
+
+@interface GIDTokenClaimsInternalOptions ()
+@property(nonatomic, readonly) id<GIDJSONSerializer> jsonSerializer;
+@end
+
+@implementation GIDTokenClaimsInternalOptions
+
+- (instancetype)init {
+  return [self initWithJSONSerializer:[[GIDJSONSerializerImpl alloc] init]];
+}
+
+- (instancetype)initWithJSONSerializer:(id<GIDJSONSerializer>)jsonSerializer {
+  if (self = [super init]) {
+    _jsonSerializer = jsonSerializer;
+  }
+  return self;
+}
+
+- (nullable NSString *)validatedJSONStringForClaims:(nullable NSSet<GIDTokenClaim *> *)claims
+                                              error:(NSError *_Nullable *_Nullable)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:kGIDTokenClaimErrorDescription
+                                 }];
+      }
+      return nil;
+    }
+    validTokenClaims[currentClaim.name] = currentClaim;
+  }
+
+  // === Step 2: Build the dictionary structure required for OIDC JSON ===
+  NSMutableDictionary<NSString *, NSDictionary *> *tokenClaimsDictionary =
+    [[NSMutableDictionary alloc] init];
+  for (GIDTokenClaim *claim in validTokenClaims.allValues) {
+    if (claim.isEssential) {
+      tokenClaimsDictionary[claim.name] = @{ kGIDTokenClaimEssentialPropertyKey: @YES };
+    } else {
+      tokenClaimsDictionary[claim.name] = @{ kGIDTokenClaimEssentialPropertyKey: @NO };
+    }
+  }
+  NSDictionary<NSString *, id> *finalRequestDictionary =
+    @{ kGIDTokenClaimKeyName: tokenClaimsDictionary };
+
+  // === Step 3: Serialize the final dictionary into a JSON string ===
+  return [_jsonSerializer stringWithJSONObject:finalRequestDictionary error:error];
+}
+
+@end

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

@@ -45,10 +45,14 @@ 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.
   kGIDSignInErrorCodeMismatchWithCurrentUser = -9,
+  /// Indicates that an object could not be serialized into a `JSON` string.
+  kGIDSignInErrorCodeJSONSerializationFailure = -10
 };
 
 /// This class is used to sign in users with their Google account and manage their session.

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

@@ -0,0 +1,113 @@
+// 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 <XCTest/XCTest.h>
+
+#import "GoogleSignIn/Sources/GIDJSONSerializer/Fake/GIDFakeJSONSerializerImpl.h"
+#import "GoogleSignIn/Sources/GIDJSONSerializer/Implementation/GIDJSONSerializerImpl.h"
+#import "GoogleSignIn/Sources/GIDTokenClaimsInternalOptions.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
+#import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDTokenClaim.h"
+
+static NSString *const kEssentialAuthTimeExpectedJSON = @"{\"id_token\":{\"auth_time\":{\"essential\":true}}}";
+static NSString *const kNonEssentialAuthTimeExpectedJSON = @"{\"id_token\":{\"auth_time\":{\"essential\":false}}}";
+
+@interface GIDTokenClaimsInternalOptionsTest : XCTestCase
+
+@property(nonatomic) GIDFakeJSONSerializerImpl *jsonSerializerFake;
+@property(nonatomic) GIDTokenClaimsInternalOptions *tokenClaimsInternalOptions;
+
+@end
+
+@implementation GIDTokenClaimsInternalOptionsTest
+
+- (void)setUp {
+  [super setUp];
+  _jsonSerializerFake = [[GIDFakeJSONSerializerImpl alloc] init];
+  _tokenClaimsInternalOptions = [[GIDTokenClaimsInternalOptions alloc] initWithJSONSerializer:_jsonSerializerFake];
+}
+
+- (void)tearDown {
+  _jsonSerializerFake = nil;
+  _tokenClaimsInternalOptions = nil;
+  [super tearDown];
+}
+
+#pragma mark - Input Validation Tests
+
+- (void)testValidatedJSONStringForClaims_WithNilInput_ShouldReturnNil {
+  XCTAssertNil([_tokenClaimsInternalOptions validatedJSONStringForClaims:nil error:nil]);
+}
+
+- (void)testValidatedJSONStringForClaims_WithEmptyInput_ShouldReturnNil {
+  XCTAssertNil([_tokenClaimsInternalOptions validatedJSONStringForClaims:[NSSet set] error:nil]);
+}
+
+#pragma mark - Correct Formatting Tests
+
+- (void)testValidatedJSONStringForClaims_WithNonEssentialClaim_IsCorrectlyFormatted {
+  NSSet *claims = [NSSet setWithObject:[GIDTokenClaim authTimeClaim]];
+  NSError *error;
+  NSString *result = [_tokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error];
+
+  XCTAssertNil(error);
+  XCTAssertEqualObjects(result, kNonEssentialAuthTimeExpectedJSON);
+}
+
+- (void)testValidatedJSONStringForClaims_WithEssentialClaim_IsCorrectlyFormatted {
+  NSSet *claims = [NSSet setWithObject:[GIDTokenClaim essentialAuthTimeClaim]];
+  NSError *error;
+  NSString *result = [_tokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error];
+
+  XCTAssertNil(error);
+  XCTAssertEqualObjects(result, kEssentialAuthTimeExpectedJSON);
+}
+
+#pragma mark - Client Error Handling Tests
+
+- (void)testValidatedJSONStringForClaims_WithConflictingClaims_ReturnsNilAndPopulatesError {
+  NSSet *claims = [NSSet setWithObjects:[GIDTokenClaim authTimeClaim],
+                                        [GIDTokenClaim essentialAuthTimeClaim],
+                                        nil];
+  NSError *error;
+  NSString *result = [_tokenClaimsInternalOptions validatedJSONStringForClaims:claims error:&error];
+
+  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 {
+  NSSet *claims = [NSSet setWithObject:[GIDTokenClaim authTimeClaim]];
+  NSError *expectedJSONError = [NSError errorWithDomain:kGIDSignInErrorDomain
+                                                   code:kGIDSignInErrorCodeJSONSerializationFailure
+                                               userInfo:@{
+                                                 NSLocalizedDescriptionKey: kGIDJSONSerializationErrorDescription,
+                                               }];
+  _jsonSerializerFake.serializationError = expectedJSONError;
+  NSError *actualError;
+  NSString *result = [_tokenClaimsInternalOptions validatedJSONStringForClaims:claims
+                                                                         error:&actualError];
+
+  XCTAssertNil(result, @"The result should be nil when JSON serialization fails.");
+  XCTAssertEqualObjects(
+      actualError,
+      expectedJSONError,
+      @"The error from serialization should be passed back to the caller."
+  );
+}
+
+@end