initial commit: Proximity iOS app proposal, full Swift code scaffold, reference backend, testing docs, generated Xcode project
This commit is contained in:
73
Proximity/Services/Auth/GoogleAuthProvider.swift
Normal file
73
Proximity/Services/Auth/GoogleAuthProvider.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user