Skip to content

Commit

Permalink
Add support for refresh tokens (#2212)
Browse files Browse the repository at this point in the history
  • Loading branch information
infomiho authored Aug 26, 2024
1 parent 0aabdda commit a6573c2
Show file tree
Hide file tree
Showing 69 changed files with 1,594 additions and 514 deletions.
83 changes: 48 additions & 35 deletions waspc/data/Generator/templates/sdk/wasp/server/auth/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{{={= =}=}}
import type { Request as ExpressRequest } from 'express'
import { type ProviderId, createUser, findAuthWithUserBy } from '../../auth/utils.js'
import { prisma } from '../index.js'
Expand Down Expand Up @@ -35,7 +36,7 @@ export type OnAfterLoginHook = (
export type InternalAuthHookParams = {
/**
* Prisma instance that can be used to interact with the database.
*/
*/
prisma: typeof prisma
}

Expand All @@ -48,86 +49,98 @@ export type InternalAuthHookParams = {
type OnBeforeSignupHookParams = {
/**
* Provider ID object that contains the provider name and the provide user ID.
*/
*/
providerId: ProviderId
/**
* Request object that can be used to access the incoming request.
*/
*/
req: ExpressRequest
} & InternalAuthHookParams

type OnAfterSignupHookParams = {
/**
* Provider ID object that contains the provider name and the provide user ID.
*/
*/
providerId: ProviderId
/**
* User object that was created during the signup process.
*/
*/
user: Awaited<ReturnType<typeof createUser>>
oauth?: {
/**
* Access token that was received during the OAuth flow.
*/
accessToken: string
/**
* Unique request ID that was generated during the OAuth flow.
*/
uniqueRequestId: string
},
/**
* OAuth flow data that was generated during the OAuth flow. This is only
* available if the user signed up using OAuth.
*/
oauth?: OAuthData
/**
* Request object that can be used to access the incoming request.
*/
*/
req: ExpressRequest
} & InternalAuthHookParams

type OnBeforeOAuthRedirectHookParams = {
/**
* URL that the OAuth flow should redirect to.
*/
*/
url: URL
/**
* Unique request ID that was generated during the OAuth flow.
*/
uniqueRequestId: string
*/
oauth: Pick<OAuthData, 'uniqueRequestId'>
/**
* Request object that can be used to access the incoming request.
*/
*/
req: ExpressRequest
} & InternalAuthHookParams

type OnBeforeLoginHookParams = {
/**
* Provider ID object that contains the provider name and the provide user ID.
*/
*/
providerId: ProviderId
/**
* Request object that can be used to access the incoming request.
*/
*/
req: ExpressRequest
} & InternalAuthHookParams

type OnAfterLoginHookParams = {
/**
* Provider ID object that contains the provider name and the provide user ID.
*/
*/
providerId: ProviderId
oauth?: {
/**
* Access token that was received during the OAuth flow.
*/
accessToken: string
/**
* Unique request ID that was generated during the OAuth flow.
*/
uniqueRequestId: string
},
/**
* User that is logged in.
*/
*/
user: Awaited<ReturnType<typeof findAuthWithUserBy>>['user']
/**
* OAuth flow data that was generated during the OAuth flow. This is only
* available if the user logged in using OAuth.
*/
oauth?: OAuthData
/**
* Request object that can be used to access the incoming request.
*/
*/
req: ExpressRequest
} & InternalAuthHookParams

// PUBLIC API
export type OAuthData = {
/**
* Unique request ID that was generated during the OAuth flow.
*/
uniqueRequestId: string
} & (
{=# enabledProviders.isGoogleAuthEnabled =}
| { providerName: 'google'; tokens: import('arctic').GoogleTokens }
{=/ enabledProviders.isGoogleAuthEnabled =}
{=# enabledProviders.isDiscordAuthEnabled =}
| { providerName: 'discord'; tokens: import('arctic').DiscordTokens }
{=/ enabledProviders.isDiscordAuthEnabled =}
{=# enabledProviders.isGitHubAuthEnabled =}
| { providerName: 'github'; tokens: import('arctic').GitHubTokens }
{=/ enabledProviders.isGitHubAuthEnabled =}
{=# enabledProviders.isKeycloakAuthEnabled =}
| { providerName: 'keycloak'; tokens: import('arctic').KeycloakTokens }
{=/ enabledProviders.isKeycloakAuthEnabled =}
| never
)
13 changes: 9 additions & 4 deletions waspc/data/Generator/templates/sdk/wasp/server/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,17 @@ export type {
OnBeforeLoginHook,
OnAfterLoginHook,
InternalAuthHookParams,
OAuthData,
} from './hooks.js'

{=# isEmailAuthEnabled =}
{=# isExternalAuthEnabled =}
export * from './oauth/index.js'
{=/ isExternalAuthEnabled =}

{=# enabledProviders.isEmailAuthEnabled =}
export * from './email/index.js'
{=/ isEmailAuthEnabled =}
{=/ enabledProviders.isEmailAuthEnabled =}

{=# isUsernameAndPasswordAuthEnabled =}
{=# enabledProviders.isUsernameAndPasswordAuthEnabled =}
export * from './username.js'
{=/ isUsernameAndPasswordAuthEnabled =}
{=/ enabledProviders.isUsernameAndPasswordAuthEnabled =}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { type ProviderConfig } from "wasp/auth/providers/types";

// PRIVATE API (SDK)
export function ensureEnvVarsForProvider<EnvVarName extends string>(
envVarNames: EnvVarName[],
provider: ProviderConfig,
providerName: string,
): Record<EnvVarName, string> {
const result: Record<string, string> = {};
for (const envVarName of envVarNames) {
const value = process.env[envVarName];
if (!value) {
throw new Error(`${envVarName} env variable is required when using the ${provider.displayName} auth provider.`);
throw new Error(`${envVarName} env variable is required when using the ${providerName} auth provider.`);
}
result[envVarName] = value;
}
Expand Down
31 changes: 31 additions & 0 deletions waspc/data/Generator/templates/sdk/wasp/server/auth/oauth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{{={= =}=}}
{=# enabledProviders.isGoogleAuthEnabled =}
// PUBLIC API
export { google } from './providers/google.js';
{=/ enabledProviders.isGoogleAuthEnabled =}
{=# enabledProviders.isDiscordAuthEnabled =}
// PUBLIC API
export { discord } from './providers/discord.js';
{=/ enabledProviders.isDiscordAuthEnabled =}
{=# enabledProviders.isGitHubAuthEnabled =}
// PUBLIC API
export { github } from './providers/github.js';
{=/ enabledProviders.isGitHubAuthEnabled =}
{=# enabledProviders.isKeycloakAuthEnabled =}
// PUBLIC API
export { keycloak } from './providers/keycloak.js';
{=/ enabledProviders.isKeycloakAuthEnabled =}

// PRIVATE API
export {
loginPath,
callbackPath,
exchangeCodeForTokenPath,
handleOAuthErrorAndGetRedirectUri,
getRedirectUriForOneTimeCode,
} from './redirect.js'

// PRIVATE API
export {
tokenStore,
} from './oneTimeCode.js'
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createJWT, validateJWT, TimeSpan } from '../../../auth/jwt.js'

export const tokenStore = createTokenStore();

function createTokenStore() {
const usedTokens = new Map<string, number>();

const validFor = new TimeSpan(1, 'm') // 1 minute
const cleanupAfter = 1000 * 60 * 60; // 1 hour

function createToken(userId: string): Promise<string> {
return createJWT(
{
id: userId,
},
{
expiresIn: validFor,
}
);
}

function verifyToken(token: string): Promise<{ id: string }> {
return validateJWT(token);
}

function isUsed(token: string): boolean {
return usedTokens.has(token);
}

function markUsed(token: string): void {
usedTokens.set(token, Date.now());
cleanUp();
}

function cleanUp(): void {
const now = Date.now();
for (const [token, timestamp] of usedTokens.entries()) {
if (now - timestamp > cleanupAfter) {
usedTokens.delete(token);
}
}
}

return {
createToken,
verifyToken,
isUsed,
markUsed,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { OAuth2Provider, OAuth2ProviderWithPKCE } from "arctic";

export function defineProvider<
OAuthClient extends OAuth2Provider | OAuth2ProviderWithPKCE,
Env extends Record<string, string>
>({
id,
displayName,
env,
oAuthClient,
}: {
id: string;
displayName: string;
env: Env;
oAuthClient: OAuthClient;
}) {
return {
id,
displayName,
env,
oAuthClient,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{{={= =}=}}
import { Discord } from "arctic";

import { defineProvider } from "../provider.js";
import { ensureEnvVarsForProvider } from "../env.js";
import { getRedirectUriForCallback } from "../redirect.js";

const id = "{= providerId =}";
const displayName = "{= displayName =}";

const env = ensureEnvVarsForProvider(
["DISCORD_CLIENT_ID", "DISCORD_CLIENT_SECRET"],
displayName
);

const oAuthClient = new Discord(
env.DISCORD_CLIENT_ID,
env.DISCORD_CLIENT_SECRET,
getRedirectUriForCallback(id).toString(),
);

// PUBLIC API
export const discord = defineProvider({
id,
displayName,
env,
oAuthClient,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{{={= =}=}}
import { GitHub } from "arctic";

import { ensureEnvVarsForProvider } from "../env.js";
import { defineProvider } from "../provider.js";

const id = "{= providerId =}";
const displayName = "{= displayName =}";

const env = ensureEnvVarsForProvider(
["GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET"],
displayName
);

const oAuthClient = new GitHub(
env.GITHUB_CLIENT_ID,
env.GITHUB_CLIENT_SECRET,
);

// PUBLIC API
export const github = defineProvider({
id,
displayName,
env,
oAuthClient,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{{={= =}=}}
import { Google } from "arctic";

import { ensureEnvVarsForProvider } from "../env.js";
import { getRedirectUriForCallback } from "../redirect.js";
import { defineProvider } from "../provider.js";

const id = "{= providerId =}";
const displayName = "{= displayName =}";

const env = ensureEnvVarsForProvider(
["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"],
displayName,
);

const oAuthClient = new Google(
env.GOOGLE_CLIENT_ID,
env.GOOGLE_CLIENT_SECRET,
getRedirectUriForCallback(id).toString(),
);

// PUBLIC API
export const google = defineProvider({
id,
displayName,
env,
oAuthClient,
});
Loading

0 comments on commit a6573c2

Please sign in to comment.