Skip to content

Instantly share code, notes, and snippets.

@darksinge
Last active July 17, 2024 05:03
Show Gist options
  • Save darksinge/e9fe765929c410c0ae987dfdf3096a72 to your computer and use it in GitHub Desktop.
Save darksinge/e9fe765929c410c0ae987dfdf3096a72 to your computer and use it in GitHub Desktop.
Custom SST Auth Adapter
import { APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
import Jwt from 'jsonwebtoken';
import { ApiHandler, useCookie } from 'sst/node/api';
import { Adapter, AuthHandler, createAdapter, useSession, Session } from 'sst/node/auth';
declare module 'sst/node/auth' {
export interface SessionTypes {
user: {
id: string
name: string
};
}
}
interface MyAdapaterConfig {
onSuccess: (claims: Record<string, any>) => Promise<APIGatewayProxyStructuredResultV2>;
onError: () => Promise<APIGatewayProxyStructuredResultV2>;
}
type MyAdapter = (config: MyAdapaterConfig) => Adapter;
const MyAdapter = createAdapter<MyAdapter>((config) => {
return async () => {
const token = useCookie('id_token');
if (!token) {
throw new Error('No token');
}
// This is NOT secure, just doing it for the sake of simplicity
const decoded = Jwt.decode(token);
if (!decoded || typeof decoded === 'string') {
throw new Error('Invalid auth request');
}
return config.onSuccess(decoded);
};
});
export const handler = AuthHandler({
providers: {
custom: MyAdapter({
// `tokenset` is the decoded JWT from line 29
onSuccess: async (tokenset) => {
const { sub, given_name, family_name } = tokenset;
return Session.cookie({
redirect: '/',
type: 'user',
properties: {
id: sub,
name: `${given_name} ${family_name}`,
},
});
},
onError: async () => {
return {
statusCode: 401,
body: 'Unauthorized',
};
},
}),
},
});

First, the Auth construct adds an /auth route to the API.

const auth = new Auth(stack, 'auth', {
  authenticator: {
    handler: 'auth.handler',
  },
});

const api = new Api(stack, 'api', {
  routes: {
    'GET /': 'index.handler',
  }
});

auth.attach(stack, { api });

A session token is generated by visiting /auth/:adapter/authorize, where :adapter is one of the keys under providers when creating an AuthHandler.

So given the following code:

export const handler = AuthHandler({
  providers: {
    foo: FooAdapter(),
    bar: BarAdapter(),
  }
})

the API will have routes for /auth/foo/authorize and /auth/bar/authorize. Note that there will also be /auth/foo|bar/callback, but for this example, the callback route is not used.

When you hit an authorize route and it's successful, you can create a token with Session.parameter({ ... }) or Session.cookie({ ... }). This example uses cookies. By default, auth-token is used for the cookie name.

It feels a little strange because you don't rely on a lambda authorizer to protect your API from unauthorized access. Instead, you use useSession() to get the current session and determine if the user is authorized (might need to double check this claim... it might happen automagically without calling useSession).

export const handler = ApiHandler(async () => {
const session = useSession();
return {
statusCode: 200,
headers: { 'content-type': 'text/plain' },
body: `Hello ${session.name}!`,
};
});
export default function MyStack({ stack }: StackContext) {
const options: ApiProps = {
routes: {
'GET /': 'src/path/to/handler.handler',
},
};
const auth = new Auth(stack, 'auth', {
authenticator: {
handler: 'src/auth/testAuth.handler',
},
});
const api = new Api(stack, 'StudentApi', options);
auth.attach(stack, { api });
stack.addOutputs({
ApiEndpoint: api.url,
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment