-
Notifications
You must be signed in to change notification settings - Fork 262
Expand file tree
/
Copy pathAuthenticationViewModel.swift
More file actions
159 lines (139 loc) · 4.83 KB
/
AuthenticationViewModel.swift
File metadata and controls
159 lines (139 loc) · 4.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/*
* Copyright 2021 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 SwiftUI
import GoogleSignIn
/// A class conforming to `ObservableObject` used to represent a user's authentication status.
final class AuthenticationViewModel: ObservableObject {
/// The user's log in status.
/// - note: This will publish updates when its value changes.
@Published var state: State
private var authenticator: GoogleSignInAuthenticator {
return GoogleSignInAuthenticator(authViewModel: self)
}
/// The user's `claims` as found in `idToken`.
/// - note: If the user is logged out, then this will default to empty.
var claims: [Claim] {
switch state {
case .signedIn(let user):
guard let idToken = user.idToken?.tokenString else { return [] }
return decodeClaims(fromJwt: idToken)
case .signedOut:
return []
}
}
/// The user-authorized scopes.
/// - note: If the user is logged out, then this will default to empty.
var authorizedScopes: [String] {
switch state {
case .signedIn(let user):
return user.grantedScopes ?? []
case .signedOut:
return []
}
}
/// Creates an instance of this view model.
init() {
if let user = GIDSignIn.sharedInstance.currentUser {
self.state = .signedIn(user)
} else {
self.state = .signedOut
}
}
/// Signs the user in.
@MainActor func signIn() {
authenticator.signIn()
}
/// Signs the user out.
func signOut() {
authenticator.signOut()
}
/// Disconnects the previously granted scope and logs the user out.
func disconnect() {
authenticator.disconnect()
}
var hasBirthdayReadScope: Bool {
return authorizedScopes.contains(BirthdayLoader.birthdayReadScope)
}
/// Adds the requested birthday read scope.
/// - parameter completion: An escaping closure that is called upon successful completion.
@MainActor func addBirthdayReadScope(completion: @escaping () -> Void) {
authenticator.addBirthdayReadScope(completion: completion)
}
}
private extension AuthenticationViewModel {
/// Returns a collection of formatted claim keys and values decoded from a JWT.
func decodeClaims(fromJwt jwt: String) -> [Claim] {
let segments = jwt.components(separatedBy: ".")
guard segments.count > 1,
let payload = decodeJWTSegment(segments[1])
else {
return []
}
let claims: [Claim?] = [
formatAuthTime(from: payload),
formatAmr(from: payload)
]
return claims.compactMap { $0 }
}
func decodeJWTSegment(_ segment: String) -> [String: Any]? {
guard let segmentData = base64UrlDecode(segment),
let segmentJSON = try? JSONSerialization.jsonObject(with: segmentData, options: []),
let payload = segmentJSON as? [String: Any] else {
return nil
}
return payload
}
func base64UrlDecode(_ value: String) -> Data? {
var base64 = value
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let length = Double(base64.lengthOfBytes(using: String.Encoding.utf8))
let requiredLength = 4 * ceil(length / 4.0)
let paddingLength = requiredLength - length
if paddingLength > 0 {
let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0)
base64 = base64 + padding
}
return Data(base64Encoded: base64, options: .ignoreUnknownCharacters)
}
/// Returns the `auth_time` claim from the given JWT, if present.
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 Claim(key: "auth_time", value: formattedDate)
}
/// Returns the `amr` claim from the given JWT, if present.
private func formatAmr(from payload: [String: Any]) -> Claim? {
guard let amr = payload["amr"] as? [String]
else {
return nil
}
return Claim(key: "amr", value: amr.joined(separator: ", "))
}
}
extension AuthenticationViewModel {
/// An enumeration representing logged in status.
enum State {
/// The user is logged in and is the associated value of this case.
case signedIn(GIDGoogleUser)
/// The user is logged out.
case signedOut
}
}