74 lines
2.8 KiB
Swift
74 lines
2.8 KiB
Swift
import Foundation
|
|
import AuthenticationServices
|
|
|
|
/// Google OAuth (via ASWebAuthenticationSession) — an alternative account
|
|
/// identity path. Google is a true OAuth provider, unlike Instagram/X which
|
|
/// are not suitable for login (see AuthService notes).
|
|
final class GoogleAuthProvider: NSObject, AuthProvider {
|
|
|
|
var providerID: AuthProviderID { .google }
|
|
|
|
private let clientID = "YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com"
|
|
private let redirectURI = "com.proximity.app:/oauth2redirect"
|
|
|
|
func signIn() async throws -> AuthResult {
|
|
// Build the Google OAuth2 authorization URL.
|
|
var components = URLComponents(string: "https://accounts.google.com/o/oauth2/v2/auth")!
|
|
components.queryItems = [
|
|
URLQueryItem(name: "client_id", value: clientID),
|
|
URLQueryItem(name: "redirect_uri", value: redirectURI),
|
|
URLQueryItem(name: "response_type", value: "code"),
|
|
URLQueryItem(name: "scope", value: "openid email profile"),
|
|
URLQueryItem(name: "nonce", value: UUID().uuidString)
|
|
]
|
|
|
|
guard let url = components.url else {
|
|
throw AuthError.invalidCredential
|
|
}
|
|
|
|
// Present the ASWebAuthenticationSession and await the callback.
|
|
let callbackURL = try await withCheckedThrowingContinuation {
|
|
(continuation: CheckedContinuation<URL, Error>) in
|
|
let session = ASWebAuthenticationSession(
|
|
url: url,
|
|
callbackURLScheme: "com.proximity.app"
|
|
) { callback, error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
} else if let callback {
|
|
continuation.resume(returning: callback)
|
|
} else {
|
|
continuation.resume(throwing: AuthError.invalidCredential)
|
|
}
|
|
}
|
|
session.presentationContextProvider = self
|
|
session.start()
|
|
}
|
|
|
|
// Extract the authorization code from the callback.
|
|
guard let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
|
|
let code = components.queryItems?.first(where: { $0.name == "code" })?.value else {
|
|
throw AuthError.invalidCredential
|
|
}
|
|
|
|
// In production: exchange `code` for an ID token at our backend,
|
|
// which verifies it with Google. Here we pass the code through.
|
|
return AuthResult(
|
|
providerID: .google,
|
|
idToken: code,
|
|
nonce: nil,
|
|
profile: ProviderProfile()
|
|
)
|
|
}
|
|
|
|
func signOut() async throws {
|
|
// Revoke handled by backend.
|
|
}
|
|
}
|
|
|
|
extension GoogleAuthProvider: ASWebAuthenticationPresentationContextProviding {
|
|
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
|
ASPresentationAnchor()
|
|
}
|
|
}
|