Skip to content

Commit

Permalink
Add OIDC examples
Browse files Browse the repository at this point in the history
  • Loading branch information
cmd-johnson committed Aug 23, 2023
1 parent 76ae9c9 commit 0f9f3d2
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 0 deletions.
103 changes: 103 additions & 0 deletions examples/oidc/http.ts
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 });
107 changes: 107 additions & 0 deletions examples/oidc/oak.ts
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 });

0 comments on commit 0f9f3d2

Please sign in to comment.