(app: Application)
| 128 | } |
| 129 | |
| 130 | setupRoutes(app: Application): void { |
| 131 | // GET /auth/login — redirect to the identity provider |
| 132 | app.get("/auth/login", async (_req, res) => { |
| 133 | try { |
| 134 | const disc = await this.getDiscovery(); |
| 135 | const state = randomBytes(16).toString("hex"); |
| 136 | this.pendingStates.set(state, Date.now()); |
| 137 | |
| 138 | const authUrl = new URL(disc.authorization_endpoint); |
| 139 | authUrl.searchParams.set("client_id", this.clientId); |
| 140 | authUrl.searchParams.set("redirect_uri", this.callbackUrl); |
| 141 | authUrl.searchParams.set("response_type", "code"); |
| 142 | authUrl.searchParams.set("scope", this.scopes); |
| 143 | authUrl.searchParams.set("state", state); |
| 144 | |
| 145 | // Store state in a short-lived cookie for CSRF validation |
| 146 | res.setHeader("Set-Cookie", [ |
| 147 | `oauth_state=${state}; HttpOnly; SameSite=Lax; Path=/auth; Max-Age=600`, |
| 148 | ]); |
| 149 | res.redirect(authUrl.toString()); |
| 150 | } catch (err) { |
| 151 | console.error("[oauth] Failed to initiate login:", err); |
| 152 | res.status(500).send("Authentication provider unavailable."); |
| 153 | } |
| 154 | }); |
| 155 | |
| 156 | // GET /auth/callback — exchange code for tokens, create session |
| 157 | app.get("/auth/callback", async (req, res) => { |
| 158 | const { code, state, error } = req.query as Record<string, string | undefined>; |
| 159 | |
| 160 | if (error) { |
| 161 | console.warn("[oauth] Provider error:", error); |
| 162 | res.status(400).send(`Provider returned error: ${error}`); |
| 163 | return; |
| 164 | } |
| 165 | |
| 166 | if (!code || !state) { |
| 167 | res.status(400).send("Missing code or state."); |
| 168 | return; |
| 169 | } |
| 170 | |
| 171 | // Validate state against cookie |
| 172 | const storedState = this.extractStateCookie(req as unknown as IncomingMessage); |
| 173 | if (!storedState || storedState !== state || !this.pendingStates.has(state)) { |
| 174 | res.status(400).send("Invalid state parameter."); |
| 175 | return; |
| 176 | } |
| 177 | this.pendingStates.delete(state); |
| 178 | // Clear state cookie |
| 179 | res.setHeader("Set-Cookie", [ |
| 180 | `oauth_state=; HttpOnly; SameSite=Lax; Path=/auth; Max-Age=0`, |
| 181 | ]); |
| 182 | |
| 183 | try { |
| 184 | const disc = await this.getDiscovery(); |
| 185 | const tokens = await this.exchangeCode(disc.token_endpoint, code); |
| 186 | const userInfo = await this.getUserInfo(disc.userinfo_endpoint, tokens.access_token); |
| 187 |
nothing calls this directly
no test coverage detected