@@ -25,6 +25,19 @@ final class AuthenticationViewModel: ObservableObject {
2525 private var authenticator : GoogleSignInAuthenticator {
2626 return GoogleSignInAuthenticator ( authViewModel: self )
2727 }
28+
29+ /// The user's `auth_time` as found in `idToken`.
30+ /// - note: If the user is logged out, then this will default to `nil`.
31+ var authTime : Date ? {
32+ switch state {
33+ case . signedIn( let user) :
34+ guard let idToken = user. idToken? . tokenString else { return nil }
35+ return decodeAuthTime ( fromJWT: idToken)
36+ case . signedOut:
37+ return nil
38+ }
39+ }
40+
2841 /// The user-authorized scopes.
2942 /// - note: If the user is logged out, then this will default to empty.
3043 var authorizedScopes : [ String ] {
@@ -69,7 +82,48 @@ final class AuthenticationViewModel: ObservableObject {
6982 @MainActor func addBirthdayReadScope( completion: @escaping ( ) -> Void ) {
7083 authenticator. addBirthdayReadScope ( completion: completion)
7184 }
85+
86+ var formattedAuthTimeString : String ? {
87+ guard let date = authTime else { return nil }
88+ let formatter = DateFormatter ( )
89+ formatter. dateFormat = " MMM d, yyyy 'at' h:mm a "
90+ return formatter. string ( from: date)
91+ }
92+ }
7293
94+ private extension AuthenticationViewModel {
95+ func decodeAuthTime( fromJWT jwt: String ) -> Date ? {
96+ let segments = jwt. components ( separatedBy: " . " )
97+ guard let parts = decodeJWTSegment ( segments [ 1 ] ) ,
98+ let authTimeInterval = parts [ " auth_time " ] as? TimeInterval else {
99+ return nil
100+ }
101+ return Date ( timeIntervalSince1970: authTimeInterval)
102+ }
103+
104+ func decodeJWTSegment( _ segment: String ) -> [ String : Any ] ? {
105+ guard let segmentData = base64UrlDecode ( segment) ,
106+ let segmentJSON = try ? JSONSerialization . jsonObject ( with: segmentData, options: [ ] ) ,
107+ let payload = segmentJSON as? [ String : Any ] else {
108+ return nil
109+ }
110+ return payload
111+ }
112+
113+ func base64UrlDecode( _ value: String ) -> Data ? {
114+ var base64 = value
115+ . replacingOccurrences ( of: " - " , with: " + " )
116+ . replacingOccurrences ( of: " _ " , with: " / " )
117+
118+ let length = Double ( base64. lengthOfBytes ( using: String . Encoding. utf8) )
119+ let requiredLength = 4 * ceil( length / 4.0 )
120+ let paddingLength = requiredLength - length
121+ if paddingLength > 0 {
122+ let padding = " " . padding ( toLength: Int ( paddingLength) , withPad: " = " , startingAt: 0 )
123+ base64 = base64 + padding
124+ }
125+ return Data ( base64Encoded: base64, options: . ignoreUnknownCharacters)
126+ }
73127}
74128
75129extension AuthenticationViewModel {
0 commit comments