-
Notifications
You must be signed in to change notification settings - Fork 265
Expand file tree
/
Copy pathGoogleSignInAuthenticator.swift
More file actions
185 lines (161 loc) · 6.17 KB
/
GoogleSignInAuthenticator.swift
File metadata and controls
185 lines (161 loc) · 6.17 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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
/*
* 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 Foundation
import GoogleSignIn
/// An observable class for authenticating via Google.
final class GoogleSignInAuthenticator: ObservableObject {
private var authViewModel: AuthenticationViewModel
private let claims: Set<GIDClaim> = [GIDClaim.essentialAMR(), GIDClaim.authTime()]
/// Creates an instance of this authenticator.
/// - parameter authViewModel: The view model this authenticator will set logged in status on.
init(authViewModel: AuthenticationViewModel) {
self.authViewModel = authViewModel
}
/// Signs in the user based upon the selected account.'
/// - note: Successful calls to this will set the `authViewModel`'s `state` property.
@MainActor func signIn() {
#if os(iOS)
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
print("There is no root view controller!")
return
}
let manualNonce = UUID().uuidString
GIDSignIn.sharedInstance.signIn(
withPresenting: rootViewController,
hint: nil,
additionalScopes: nil,
nonce: manualNonce,
claims: claims
) { signInResult, error in
guard let signInResult = signInResult else {
print("Error! \(String(describing: error))")
return
}
// Per OpenID Connect Core section 3.1.3.7, rule #11, compare returned nonce to manual
guard let idToken = signInResult.user.idToken?.tokenString,
let returnedNonce = self.decodeNonce(fromJWT: idToken),
returnedNonce == manualNonce else {
// Assert a failure for convenience so that integration tests with this sample app fail upon
// `nonce` mismatch
assertionFailure("ERROR: Returned nonce doesn't match manual nonce!")
return
}
self.authViewModel.state = .signedIn(signInResult.user)
}
#elseif os(macOS)
guard let presentingWindow = NSApplication.shared.windows.first else {
print("There is no presenting window!")
return
}
GIDSignIn.sharedInstance.signIn(
withPresenting: presentingWindow,
claims: claims
) { signInResult, error in
guard let signInResult = signInResult else {
print("Error! \(String(describing: error))")
return
}
self.authViewModel.state = .signedIn(signInResult.user)
}
#endif
}
/// Signs out the current user.
func signOut() {
GIDSignIn.sharedInstance.signOut()
authViewModel.state = .signedOut
}
/// Disconnects the previously granted scope and signs the user out.
func disconnect() {
GIDSignIn.sharedInstance.disconnect { error in
if let error = error {
print("Encountered error disconnecting scope: \(error).")
}
self.signOut()
}
}
// Confines birthday calucation to iOS for now.
/// Adds the birthday read scope for the current user.
/// - parameter completion: An escaping closure that is called upon successful completion of the
/// `addScopes(_:presenting:)` request.
/// - note: Successful requests will update the `authViewModel.state` with a new current user that
/// has the granted scope.
@MainActor func addBirthdayReadScope(completion: @escaping () -> Void) {
guard let currentUser = GIDSignIn.sharedInstance.currentUser else {
fatalError("No user signed in!")
}
#if os(iOS)
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
fatalError("No root view controller!")
}
currentUser.addScopes([BirthdayLoader.birthdayReadScope],
presenting: rootViewController) { signInResult, error in
if let error = error {
print("Found error while adding birthday read scope: \(error).")
return
}
guard let signInResult = signInResult else { return }
self.authViewModel.state = .signedIn(signInResult.user)
completion()
}
#elseif os(macOS)
guard let presentingWindow = NSApplication.shared.windows.first else {
fatalError("No presenting window!")
}
currentUser.addScopes([BirthdayLoader.birthdayReadScope],
presenting: presentingWindow) { signInResult, error in
if let error = error {
print("Found error while adding birthday read scope: \(error).")
return
}
guard let signInResult = signInResult else { return }
self.authViewModel.state = .signedIn(signInResult.user)
completion()
}
#endif
}
}
// MARK: Parse nonce from JWT ID Token
private extension GoogleSignInAuthenticator {
func decodeNonce(fromJWT jwt: String) -> String? {
let segments = jwt.components(separatedBy: ".")
guard let parts = decodeJWTSegment(segments[1]),
let nonce = parts["nonce"] as? String else {
return nil
}
return nonce
}
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)
}
}