From 22866a86eb1a3887afa6e1ea37c6def26cb4bc16 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:05:49 -0700 Subject: [PATCH 1/6] Updated DaysUntilBirthday app to support amr --- .../Services/GoogleSignInAuthenticator.swift | 2 +- .../ViewModels/AuthenticationViewModel.swift | 57 +++++++++++++------ .../iOS/UserProfileView.swift | 33 ++++++++++- 3 files changed, 73 insertions(+), 19 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift index afa9ba49..48a0175f 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Services/GoogleSignInAuthenticator.swift @@ -20,7 +20,7 @@ import GoogleSignIn /// An observable class for authenticating via Google. final class GoogleSignInAuthenticator: ObservableObject { private var authViewModel: AuthenticationViewModel - private var claims: Set = Set([GIDClaim.authTime()]) + private let claims: Set = [GIDClaim.essentialAMR(), GIDClaim.authTime()] /// Creates an instance of this authenticator. /// - parameter authViewModel: The view model this authenticator will set logged in status on. diff --git a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift index b528fc68..5288d1b1 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift @@ -26,15 +26,15 @@ final class AuthenticationViewModel: ObservableObject { return GoogleSignInAuthenticator(authViewModel: self) } - /// The user's `auth_time` as found in `idToken`. - /// - note: If the user is logged out, then this will default to `nil`. - var authTime: Date? { + /// The user's `claims` as found in `idToken`. + /// - note: If the user is logged out, then this will default to empty. + var claims: [(key: String, value: String)] { switch state { case .signedIn(let user): - guard let idToken = user.idToken?.tokenString else { return nil } - return decodeAuthTime(fromJWT: idToken) + guard let idToken = user.idToken?.tokenString else { return [] } + return decodeClaims(fromJwt: idToken) case .signedOut: - return nil + return [] } } @@ -82,23 +82,48 @@ final class AuthenticationViewModel: ObservableObject { @MainActor func addBirthdayReadScope(completion: @escaping () -> Void) { authenticator.addBirthdayReadScope(completion: completion) } - - var formattedAuthTimeString: String? { - guard let date = authTime else { return nil } - let formatter = DateFormatter() - formatter.dateFormat = "MMM d, yyyy 'at' h:mm a" - return formatter.string(from: date) - } } private extension AuthenticationViewModel { - func decodeAuthTime(fromJWT jwt: String) -> Date? { + /// Returns the `auth_time` claim from the given JWT, if present. + func decodeAuthTime(fromJwt jwt: String) -> Date? { + let segments = jwt.components(separatedBy: ".") + guard let parts = decodeJWTSegment(segments[1]), + let authTime = parts["auth_time"] as? TimeInterval + else { + return nil + } + return Date(timeIntervalSince1970: authTime) + } + + /// Returns the `amr` claim from the given JWT, if present. + func decodeAmr(fromJwt jwt: String) -> [String]? { let segments = jwt.components(separatedBy: ".") guard let parts = decodeJWTSegment(segments[1]), - let authTimeInterval = parts["auth_time"] as? TimeInterval else { + let amr = parts["amr"] as? [String] + else { return nil } - return Date(timeIntervalSince1970: authTimeInterval) + return amr + } + + /// Returns a collection of formatted claim keys and values decoded from a JWT. + func decodeClaims(fromJwt jwt: String) -> [(key: String, value: String)] { + let claims: [(key: String, value: String)?] = [ + decodeAuthTime(fromJwt: jwt).map { date in + let formattedDate = DateFormatter.localizedString( + from: date, + dateStyle: .medium, + timeStyle: .medium + ) + return (key: "auth_time", value: formattedDate) + }, + decodeAmr(fromJwt: jwt).map { amr in + (key: "amr", value: amr.joined(separator: ", ")) + }, + ] + + return claims.compactMap { $0 } } func decodeJWTSegment(_ segment: String) -> [String: Any]? { diff --git a/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift b/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift index 256b777b..51281e15 100644 --- a/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift +++ b/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift @@ -17,6 +17,27 @@ import SwiftUI import GoogleSignIn +/// A view that displays a list of ID token claims. +struct ClaimsListView: View { + let claims: [(key: String, value: String)] + var body: some View { + List(claims, id: \.key) { claim in + VStack(alignment: .leading, spacing: 4) { + Text(claim.key) + .font(.caption) + .foregroundColor(.secondary) + .fontWeight(.semibold) + + Text(claim.value) + .font(.body) + .lineLimit(4) + } + .padding(.vertical, 4) + } + .navigationTitle("ID Token Claims") + } +} + struct UserProfileView: View { @EnvironmentObject var authViewModel: AuthenticationViewModel @StateObject var birthdayViewModel = BirthdayViewModel() @@ -35,8 +56,16 @@ struct UserProfileView: View { Text(userProfile.name) .font(.headline) Text(userProfile.email) - if let authTimeString = authViewModel.formattedAuthTimeString { - Text("Last sign-in date: \(authTimeString)") + + if !authViewModel.claims.isEmpty { + NavigationLink(destination: ClaimsListView(claims: authViewModel.claims)) { + HStack { + Image(systemName: "list.bullet.rectangle.portrait") + Text("View ID Token Claims") + } + .font(.caption) + .foregroundColor(.blue) + } } } } From 0d6f0e86372ba3ee86652c67c055845b2569533c Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:25:13 -0700 Subject: [PATCH 2/6] Updated DaysUntilBirthday app to support amr --- .../ViewModels/AuthenticationViewModel.swift | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift index 5288d1b1..59613d1b 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift @@ -85,42 +85,19 @@ final class AuthenticationViewModel: ObservableObject { } private extension AuthenticationViewModel { - /// Returns the `auth_time` claim from the given JWT, if present. - func decodeAuthTime(fromJwt jwt: String) -> Date? { + /// Returns a collection of formatted claim keys and values decoded from a JWT. + func decodeClaims(fromJwt jwt: String) -> [(key: String, value: String)] { let segments = jwt.components(separatedBy: ".") - guard let parts = decodeJWTSegment(segments[1]), - let authTime = parts["auth_time"] as? TimeInterval - else { - return nil - } - return Date(timeIntervalSince1970: authTime) - } - /// Returns the `amr` claim from the given JWT, if present. - func decodeAmr(fromJwt jwt: String) -> [String]? { - let segments = jwt.components(separatedBy: ".") - guard let parts = decodeJWTSegment(segments[1]), - let amr = parts["amr"] as? [String] + guard segments.count > 1, + let payload = decodeJWTSegment(segments[1]) else { - return nil + return [] } - return amr - } - /// Returns a collection of formatted claim keys and values decoded from a JWT. - func decodeClaims(fromJwt jwt: String) -> [(key: String, value: String)] { let claims: [(key: String, value: String)?] = [ - decodeAuthTime(fromJwt: jwt).map { date in - let formattedDate = DateFormatter.localizedString( - from: date, - dateStyle: .medium, - timeStyle: .medium - ) - return (key: "auth_time", value: formattedDate) - }, - decodeAmr(fromJwt: jwt).map { amr in - (key: "amr", value: amr.joined(separator: ", ")) - }, + formatAuthTime(from: payload), + formatAmr(from: payload) ] return claims.compactMap { $0 } @@ -149,6 +126,26 @@ private extension AuthenticationViewModel { } return Data(base64Encoded: base64, options: .ignoreUnknownCharacters) } + + /// Returns the `auth_time` claim from the given JWT, if present. + func formatAuthTime(from payload: [String: Any]) -> (key: String, value: String)? { + guard let authTime = payload["auth_time"] as? TimeInterval + else { + return nil + } + let date = Date(timeIntervalSince1970: authTime) + let formattedDate = DateFormatter.localizedString(from: date, dateStyle: .medium, timeStyle: .medium) + return (key: "auth_time", value: formattedDate) + } + + /// Returns the `amr` claim from the given JWT, if present. + private func formatAmr(from payload: [String: Any]) -> (key: String, value: String)? { + guard let amr = payload["amr"] as? [String] + else { + return nil + } + return (key: "amr", value: amr.joined(separator: ", ")) + } } extension AuthenticationViewModel { From 97ca80074033a9fb75124a7583925a24d93eac88 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:41:49 -0700 Subject: [PATCH 3/6] Added a Claim type to the implementation --- .../DaysUntilBirthday/Shared/Models/Claim.swift | 6 ++++++ .../ViewModels/AuthenticationViewModel.swift | 14 +++++++------- .../DaysUntilBirthday/iOS/UserProfileView.swift | 5 ++--- 3 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift b/Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift new file mode 100644 index 00000000..7ba46588 --- /dev/null +++ b/Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift @@ -0,0 +1,6 @@ + +struct Claim: Identifiable { + let key: String + let value: String + var id: String {key} +} diff --git a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift index 59613d1b..868a978d 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/ViewModels/AuthenticationViewModel.swift @@ -28,7 +28,7 @@ final class AuthenticationViewModel: ObservableObject { /// The user's `claims` as found in `idToken`. /// - note: If the user is logged out, then this will default to empty. - var claims: [(key: String, value: String)] { + var claims: [Claim] { switch state { case .signedIn(let user): guard let idToken = user.idToken?.tokenString else { return [] } @@ -86,7 +86,7 @@ final class AuthenticationViewModel: ObservableObject { private extension AuthenticationViewModel { /// Returns a collection of formatted claim keys and values decoded from a JWT. - func decodeClaims(fromJwt jwt: String) -> [(key: String, value: String)] { + func decodeClaims(fromJwt jwt: String) -> [Claim] { let segments = jwt.components(separatedBy: ".") guard segments.count > 1, @@ -95,7 +95,7 @@ private extension AuthenticationViewModel { return [] } - let claims: [(key: String, value: String)?] = [ + let claims: [Claim?] = [ formatAuthTime(from: payload), formatAmr(from: payload) ] @@ -128,23 +128,23 @@ private extension AuthenticationViewModel { } /// Returns the `auth_time` claim from the given JWT, if present. - func formatAuthTime(from payload: [String: Any]) -> (key: String, value: String)? { + func formatAuthTime(from payload: [String: Any]) -> Claim? { guard let authTime = payload["auth_time"] as? TimeInterval else { return nil } let date = Date(timeIntervalSince1970: authTime) let formattedDate = DateFormatter.localizedString(from: date, dateStyle: .medium, timeStyle: .medium) - return (key: "auth_time", value: formattedDate) + return Claim(key: "auth_time", value: formattedDate) } /// Returns the `amr` claim from the given JWT, if present. - private func formatAmr(from payload: [String: Any]) -> (key: String, value: String)? { + private func formatAmr(from payload: [String: Any]) -> Claim? { guard let amr = payload["amr"] as? [String] else { return nil } - return (key: "amr", value: amr.joined(separator: ", ")) + return Claim(key: "amr", value: amr.joined(separator: ", ")) } } diff --git a/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift b/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift index 51281e15..16392cc5 100644 --- a/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift +++ b/Samples/Swift/DaysUntilBirthday/iOS/UserProfileView.swift @@ -19,9 +19,9 @@ import GoogleSignIn /// A view that displays a list of ID token claims. struct ClaimsListView: View { - let claims: [(key: String, value: String)] + let claims: [Claim] var body: some View { - List(claims, id: \.key) { claim in + List(claims) { claim in VStack(alignment: .leading, spacing: 4) { Text(claim.key) .font(.caption) @@ -63,7 +63,6 @@ struct UserProfileView: View { Image(systemName: "list.bullet.rectangle.portrait") Text("View ID Token Claims") } - .font(.caption) .foregroundColor(.blue) } } From 06e63a8acdb265e91344a1b52b228d0581897a77 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:06:52 -0700 Subject: [PATCH 4/6] Added a Claim type to the implementation --- .../Shared/Models/Claim.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift b/Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift index 7ba46588..7a08e7f3 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift @@ -1,4 +1,22 @@ +/* + * Copyright 2026 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 + +/// A decoded representation of a single ID token claim. struct Claim: Identifiable { let key: String let value: String From aa0cdbe229283e05e1645df6dd2b3a98935b3b76 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:21:49 -0700 Subject: [PATCH 5/6] Updated DaysUntilBirthday.xcodeproj/project.pbxproj file to include target membership of the new Claim type --- .../DaysUntilBirthday.xcodeproj/project.pbxproj | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj index 4c12414e..7d947429 100644 --- a/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj +++ b/Samples/Swift/DaysUntilBirthday/DaysUntilBirthday.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -40,6 +40,9 @@ 73DB41932805FC3B0028B8D3 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7173A527F5110F00910319 /* UserProfileView.swift */; }; 73DB41952805FC5F0028B8D3 /* Birthday.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739FCC47270E659A00C92042 /* Birthday.swift */; }; 73DB419628060A9A0028B8D3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7345AD062703D9480020AFB1 /* Assets.xcassets */; }; + F8CE667E2F86AB020044EAFF /* Claim.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CE667D2F86AAFD0044EAFF /* Claim.swift */; }; + F8CE667F2F86AB020044EAFF /* Claim.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CE667D2F86AAFD0044EAFF /* Claim.swift */; }; + F8CE66802F86B6A30044EAFF /* Claim.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CE667D2F86AAFD0044EAFF /* Claim.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -73,6 +76,7 @@ 739FCC45270E467600C92042 /* BirthdayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BirthdayView.swift; sourceTree = ""; }; 739FCC47270E659A00C92042 /* Birthday.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Birthday.swift; sourceTree = ""; }; 73DB417E2805F9850028B8D3 /* GoogleSignIn-iOS */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "GoogleSignIn-iOS"; path = ../../..; sourceTree = ""; }; + F8CE667D2F86AAFD0044EAFF /* Claim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Claim.swift; sourceTree = ""; }; FE2F2ABC2800D9C1005EA17F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FE71738027ECFAF400910319 /* DaysUntilBirthday (macOS).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DaysUntilBirthday (macOS).app"; sourceTree = BUILT_PRODUCTS_DIR; }; FE71738927ECFAF600910319 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -212,6 +216,7 @@ FE7173AD27F65B8500910319 /* Models */ = { isa = PBXGroup; children = ( + F8CE667D2F86AAFD0044EAFF /* Claim.swift */, 739FCC47270E659A00C92042 /* Birthday.swift */, ); path = Models; @@ -378,6 +383,7 @@ 739FCC46270E467600C92042 /* BirthdayView.swift in Sources */, 7345AD1B2703D9C30020AFB1 /* SignInView.swift in Sources */, 7345AD212703D9C30020AFB1 /* GoogleSignInAuthenticator.swift in Sources */, + F8CE667E2F86AB020044EAFF /* Claim.swift in Sources */, 7345AD232703D9C30020AFB1 /* UserProfileImageLoader.swift in Sources */, 7345AD1E2703D9C30020AFB1 /* UserProfileImageView.swift in Sources */, 736F49BC270E102C00580053 /* BirthdayViewModel.swift in Sources */, @@ -394,6 +400,7 @@ buildActionMask = 2147483647; files = ( 73508ED528134C7300ED7FB7 /* Credential.swift in Sources */, + F8CE66802F86B6A30044EAFF /* Claim.swift in Sources */, 73508EC82811BD9C00ED7FB7 /* DaysUntilBirthdayUITests_iOS.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -406,6 +413,7 @@ 73DB418B2805FBC40028B8D3 /* BirthdayLoader.swift in Sources */, 73DB418D2805FBD00028B8D3 /* AuthenticationViewModel.swift in Sources */, 73DB418F2805FBF50028B8D3 /* ContentView.swift in Sources */, + F8CE667F2F86AB020044EAFF /* Claim.swift in Sources */, 73DB418C2805FBC80028B8D3 /* UserProfileImageLoader.swift in Sources */, 73DB418E2805FBD40028B8D3 /* BirthdayViewModel.swift in Sources */, 73DB41952805FC5F0028B8D3 /* Birthday.swift in Sources */, From 823d9ed8f17a96ba31ab0cba20fa1b9c88e560d4 Mon Sep 17 00:00:00 2001 From: Akshat Gandhi <54901287+AkshatG6@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:32:59 -0700 Subject: [PATCH 6/6] Updated styling in type --- Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift b/Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift index 7a08e7f3..f6164ca4 100644 --- a/Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift +++ b/Samples/Swift/DaysUntilBirthday/Shared/Models/Claim.swift @@ -20,5 +20,5 @@ import Foundation struct Claim: Identifiable { let key: String let value: String - var id: String {key} + var id: String { key } }