eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2
3// πŸ€– The web-flow doors β€” Google, Microsoft, GitHub β€” as native sign-ins
4// (the client half of home/oidc.go + github.go; the server legs shipped
5// first). Unlike Apple, these have no native sheet on any platform: each
6// app runs an ASWebAuthenticationSession over the provider's authorize URL,
7// home's /oidc/return trampoline bounces the result to
8// elevenmessenger://home-oidc, and the session hands that callback straight
9// back IN-PROCESS β€” the scheme match is the session's own, not Launch
10// Services', so alt bundles with suffixed schemes work unchanged and no
11// Info.plist or onOpenURL change is needed.
12//
13// This file is the pure half β€” URL builders and the callback parser β€”
14// shared by both platforms and pinned by HomeOIDCAuthTests against home's
15// Go trampoline tests (oidc_test.go / github_test.go).
16public enum OIDCDoor: String, CaseIterable, Sendable {
17 case google, microsoft, github
18
19 public var display: String {
20 switch self {
21 case .google: return "Google"
22 case .microsoft: return "Microsoft"
23 case .github: return "GitHub"
24 }
25 }
26}
27
28public enum HomeOIDCAuth {
29 /// A provider's client id is a public identifier and lives here in code,
30 /// mirroring the web (home.js) verbatim. GitHub's is per-instance (prod
31 /// and dev are separate GitHub OAuth apps) and rides /api/features'
32 /// doors block instead β€” see HomeClient.signinDoors().
33 public static let googleClientID =
34 "981276018948-b4cr3ctqi6cr9e8rs2fmqouip1hmd9iq.apps.googleusercontent.com"
35 public static let microsoftClientID = "25874cf3-213c-4124-9641-65de8a513787"
36
37 /// ASWebAuthenticationSession's callbackURLScheme β€” the scheme half of
38 /// home's oidcReturnScheme (elevenmessenger://home-oidc). Deliberately
39 /// NOT the bundle's (possibly alt-suffixed) registered scheme: the
40 /// session matches this string in-process.
41 public static let callbackScheme = "elevenmessenger"
42
43 /// The authorize URL a browser session opens. `state` must NOT start
44 /// with "web." β€” that prefix routes home's trampoline back to the web
45 /// page; anything else bounces to the native scheme. Google/Microsoft
46 /// run the OIDC id_token form_post flow (nonce required β€” send the SAME
47 /// string in the verify body so home checks the round trip); GitHub is
48 /// the code-exchange flow, no nonce β€” the server holds the secret and
49 /// swaps the code itself. Nil only for GitHub without a client id (the
50 /// door is dark on that home server).
51 public static func authorizeURL(_ door: OIDCDoor, base: URL, state: String,
52 nonce: String, githubClientID: String? = nil) -> URL? {
53 let redirect = base.appending(path: "/oidc/return").absoluteString
54 var c: URLComponents
55 switch door {
56 case .google:
57 c = URLComponents(string: "https://accounts.google.com/o/oauth2/v2/auth")!
58 c.queryItems = idTokenQuery(clientID: googleClientID, redirect: redirect,
59 state: state, nonce: nonce)
60 case .microsoft:
61 // The "common" endpoint admits any tenant plus personal
62 // accounts β€” oidc.go's checkIssuer handles the per-tenant iss.
63 c = URLComponents(string: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize")!
64 c.queryItems = idTokenQuery(clientID: microsoftClientID, redirect: redirect,
65 state: state, nonce: nonce)
66 case .github:
67 guard let id = githubClientID, !id.isEmpty else { return nil }
68 c = URLComponents(string: "https://github.com/login/oauth/authorize")!
69 c.queryItems = [
70 URLQueryItem(name: "client_id", value: id),
71 URLQueryItem(name: "redirect_uri", value: redirect),
72 URLQueryItem(name: "state", value: state),
73 ]
74 }
75 return c.url
76 }
77
78 private static func idTokenQuery(clientID: String, redirect: String,
79 state: String, nonce: String) -> [URLQueryItem] {
80 [
81 URLQueryItem(name: "client_id", value: clientID),
82 URLQueryItem(name: "redirect_uri", value: redirect),
83 URLQueryItem(name: "response_type", value: "id_token"),
84 URLQueryItem(name: "response_mode", value: "form_post"),
85 URLQueryItem(name: "scope", value: "openid"),
86 URLQueryItem(name: "nonce", value: nonce),
87 URLQueryItem(name: "state", value: state),
88 ]
89 }
90
91 /// What the trampoline bounced back: an id_token (Google/Microsoft) or a
92 /// single-use code (GitHub) β€” both ride the URL FRAGMENT, kept out of
93 /// logs, parsed with query semantics exactly like the Mac SiwA callback.
94 public struct Callback: Equatable {
95 public let idToken: String?
96 public let code: String?
97 }
98
99 /// Nil when the URL isn't ours, the state doesn't round-trip, or no
100 /// credential rode along β€” every nil is "start over", never a crash.
101 public static func callback(_ url: URL, state expected: String) -> Callback? {
102 guard url.host == "home-oidc",
103 let raw = URLComponents(url: url, resolvingAgainstBaseURL: false)?.percentEncodedFragment
104 else { return nil }
105 var frag = URLComponents()
106 frag.percentEncodedQuery = raw
107 let items = frag.queryItems ?? []
108 func value(_ name: String) -> String? { items.first { $0.name == name }?.value }
109 guard value("state") == expected else { return nil }
110 let idToken = value("id_token")
111 let code = value("code")
112 guard idToken != nil || code != nil else { return nil }
113 return Callback(idToken: idToken, code: code)
114 }
115}
116