-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
76ae9c9
commit 0f9f3d2
Showing
2 changed files
with
210 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { serve } from "https://deno.land/[email protected]/http/server.ts"; | ||
import { | ||
Cookie, | ||
deleteCookie, | ||
getCookies, | ||
setCookie, | ||
} from "https://deno.land/[email protected]/http/cookie.ts"; | ||
import * as jose from "https://deno.land/x/[email protected]/index.ts"; | ||
import { OIDCClient } from "../../src/oidc/oidc_client.ts"; | ||
|
||
const jwks = jose.createRemoteJWKSet( | ||
new URL("https://www.googleapis.com/oauth2/v3/certs"), | ||
); | ||
|
||
const oidcClient = new OIDCClient({ | ||
clientId: Deno.env.get("CLIENT_ID")!, | ||
clientSecret: Deno.env.get("CLIENT_SECRET")!, | ||
authorizationEndpointUri: "https://accounts.google.com/o/oauth2/v2/auth", | ||
tokenUri: "https://oauth2.googleapis.com/token", | ||
redirectUri: "http://localhost:8000/oauth2/callback", | ||
userInfoEndpoint: "https://openidconnect.googleapis.com/v1/userinfo", | ||
defaults: { | ||
scope: ["openid", "email", "profile"], | ||
}, | ||
verifyJwt: (jwt) => jose.jwtVerify(jwt, jwks), | ||
}); | ||
|
||
/** This is where we'll store our state and PKCE codeVerifiers */ | ||
const loginStates = new Map<string, { state: string; codeVerifier: string }>(); | ||
/** The name we'll use for the session cookie */ | ||
const cookieName = "session"; | ||
|
||
/** Handles incoming HTTP requests */ | ||
function handler(req: Request): Promise<Response> | Response { | ||
const url = new URL(req.url); | ||
const path = url.pathname; | ||
|
||
switch (path) { | ||
case "/login": | ||
return redirectToAuthEndpoint(); | ||
case "/oauth2/callback": | ||
return handleCallback(req); | ||
default: | ||
return new Response("Not Found", { status: 404 }); | ||
} | ||
} | ||
|
||
async function redirectToAuthEndpoint(): Promise<Response> { | ||
// Generate a random state | ||
const state = crypto.randomUUID(); | ||
|
||
const { uri, codeVerifier } = await oidcClient.code.getAuthorizationUri({ | ||
state, | ||
}); | ||
|
||
// Associate the state and PKCE codeVerifier with a session cookie | ||
const sessionId = crypto.randomUUID(); | ||
loginStates.set(sessionId, { state, codeVerifier }); | ||
const sessionCookie: Cookie = { | ||
name: cookieName, | ||
value: sessionId, | ||
httpOnly: true, | ||
sameSite: "Lax", | ||
}; | ||
const headers = new Headers({ Location: uri.toString() }); | ||
setCookie(headers, sessionCookie); | ||
|
||
// Redirect to the authorization endpoint | ||
return new Response(null, { status: 302, headers }); | ||
} | ||
|
||
async function handleCallback(req: Request): Promise<Response> { | ||
// Load the state and PKCE codeVerifier associated with the session | ||
const sessionCookie = getCookies(req.headers)[cookieName]; | ||
const loginState = sessionCookie && loginStates.get(sessionCookie); | ||
if (!loginState) { | ||
throw new Error("invalid session"); | ||
} | ||
loginStates.delete(sessionCookie); | ||
|
||
// Exchange the authorization code for an access token | ||
const tokens = await oidcClient.code.getToken(req.url, loginState); | ||
|
||
const userInfo = await oidcClient.getUserInfo( | ||
tokens.accessToken, | ||
tokens.idToken, | ||
); | ||
|
||
// Clear the session cookie since we don't need it anymore | ||
const headers = new Headers(); | ||
deleteCookie(headers, cookieName); | ||
return new Response( | ||
`<!DOCTYPE html><html><body><h1>Hello, ${userInfo.name}!</h1></body></html>`, | ||
{ | ||
headers: { | ||
"content-type": "text/html", | ||
}, | ||
}, | ||
); | ||
} | ||
|
||
// Start the app | ||
serve(handler, { port: 8000 }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { Application, Router } from "https://deno.land/x/[email protected]/mod.ts"; | ||
import { | ||
MemoryStore, | ||
Session, | ||
} from "https://deno.land/x/[email protected]/mod.ts"; | ||
import * as jose from "https://deno.land/x/[email protected]/index.ts"; | ||
import { OIDCClient } from "../../src/oidc/oidc_client.ts"; | ||
|
||
const jwks = jose.createRemoteJWKSet( | ||
new URL("https://www.googleapis.com/oauth2/v3/certs"), | ||
); | ||
|
||
const oidcClient = new OIDCClient({ | ||
clientId: Deno.env.get("CLIENT_ID")!, | ||
clientSecret: Deno.env.get("CLIENT_SECRET")!, | ||
authorizationEndpointUri: "https://accounts.google.com/o/oauth2/v2/auth", | ||
tokenUri: "https://oauth2.googleapis.com/token", | ||
redirectUri: "http://localhost:8000/oauth2/callback", | ||
userInfoEndpoint: "https://openidconnect.googleapis.com/v1/userinfo", | ||
defaults: { | ||
scope: ["openid", "email", "profile"], | ||
}, | ||
verifyJwt: (jwt) => jose.jwtVerify(jwt, jwks), | ||
}); | ||
|
||
type AppState = { | ||
session: Session; | ||
}; | ||
|
||
const router = new Router<AppState>(); | ||
router.get("/login", async (ctx) => { | ||
// Generate a random state for this login event | ||
const state = crypto.randomUUID(); | ||
|
||
// Construct the URL for the authorization redirect and get a PKCE codeVerifier | ||
const { uri, codeVerifier } = await oidcClient.code.getAuthorizationUri({ | ||
state, | ||
}); | ||
|
||
// Store both the state and codeVerifier in the user session | ||
ctx.state.session.flash("state", state); | ||
ctx.state.session.flash("codeVerifier", codeVerifier); | ||
|
||
// Redirect the user to the authorization endpoint | ||
ctx.response.redirect(uri); | ||
}); | ||
router.get("/oauth2/callback", async (ctx) => { | ||
// Make sure both a state and codeVerifier are present for the user's session | ||
const state = ctx.state.session.get("state"); | ||
if (typeof state !== "string") { | ||
throw new Error("invalid state"); | ||
} | ||
|
||
const codeVerifier = ctx.state.session.get("codeVerifier"); | ||
if (typeof codeVerifier !== "string") { | ||
throw new Error("invalid codeVerifier"); | ||
} | ||
|
||
// Exchange the authorization code for an access token | ||
const tokens = await oidcClient.code.getToken(ctx.request.url, { | ||
state, | ||
codeVerifier, | ||
}); | ||
|
||
// Use the userinfo endpoint to get more information about the user | ||
const userInfo = await oidcClient.getUserInfo( | ||
tokens.accessToken, | ||
tokens.idToken, | ||
); | ||
|
||
ctx.response.headers.set("Content-Type", "text/html"); | ||
ctx.response.body = | ||
`<!DOCTYPE html><html><body><h1>Hello, ${userInfo.name}!</h1></body></html>`; | ||
}); | ||
|
||
const app = new Application<AppState>(); | ||
|
||
// Add a key for signing cookies | ||
app.keys = ["super-secret-key"]; | ||
|
||
// Set up the session middleware | ||
const sessionStore = new MemoryStore(); | ||
app.use(Session.initMiddleware(sessionStore, { | ||
cookieSetOptions: { | ||
httpOnly: true, | ||
sameSite: "lax", | ||
// Enable for when running outside of localhost | ||
// secure: true, | ||
signed: true, | ||
}, | ||
cookieGetOptions: { | ||
signed: true, | ||
}, | ||
expireAfterSeconds: 60 * 10, | ||
})); | ||
|
||
// Mount the router | ||
app.use(router.allowedMethods(), router.routes()); | ||
|
||
// Start the app | ||
const port = 8000; | ||
app.addEventListener("listen", () => { | ||
console.log( | ||
`App listening on port ${port}. Navigate to http://localhost:${port}/login to log in!`, | ||
); | ||
}); | ||
await app.listen({ port }); |