Spaces:
Build error
Build error
import { NextAuthOptions } from 'next-auth'; | |
import GoogleProvider from 'next-auth/providers/google'; | |
import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next"; | |
import { getServerSession } from "next-auth"; | |
import { SupabaseAdapter } from "@auth/supabase-adapter"; | |
import type { Adapter } from 'next-auth/adapters'; | |
import jwt from "jsonwebtoken"; | |
import { JWE, JWK } from 'node-jose'; | |
const makeUserinfoRequest = async (context: any) => { | |
console.log('context:', context); | |
const response = await fetch("https://api.id.gov.sg/v2/oauth/userinfo", { | |
headers: { | |
Authorization: `Bearer ${context.tokens.access_token}`, | |
}, | |
}) | |
const profile = await response.json() | |
// Decrypt the encrypted profile data | |
const privateKey = process.env.SGID_PRIVATE_KEY as string; | |
const decryptedProfile = await decryptData(profile.key, profile.data, privateKey); | |
// Build profile object | |
let newProfile = { | |
sub: profile.sub, | |
name: decryptedProfile['myinfo.name'], | |
} | |
// Return the decrypted profile | |
return newProfile | |
} | |
const decryptData = async (encKey: string, block: { [s: string]: unknown; } | ArrayLike<unknown>, privateKeyPem: string | object | Buffer | JWK.RawKey) => { | |
const result: { [key: string]: string } = {}; | |
// Decrypted encKey to get block key | |
const privateKey = await JWK.asKey(privateKeyPem, 'pem'); | |
const key = await JWE.createDecrypt(privateKey).decrypt(encKey); | |
// Parse the block key | |
const decryptedKey = await JWK.asKey(key.plaintext, 'json'); | |
// Decrypt data | |
for (const [key, value] of Object.entries(block)) { | |
const { plaintext } = await JWE.createDecrypt(decryptedKey).decrypt(value as string); | |
result[key] = plaintext.toString('ascii'); | |
} | |
return result | |
} | |
// You'll need to import and pass this | |
// to `NextAuth` in `app/api/auth/[...nextauth]/route.ts` | |
export const config = { | |
// Configure one or more authentication providers | |
// Enable debug messages if running in development | |
debug: process.env.NODE_ENV === 'development', | |
providers: [ | |
GoogleProvider({ | |
clientId: process.env.GOOGLE_CLIENT_ID as string, | |
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, | |
}), | |
{ | |
id: 'sgid', | |
name: 'SGID', | |
type: 'oauth', | |
issuer: 'https://api.id.gov.sg/v2', | |
authorization: { | |
url: 'https://api.id.gov.sg/v2/oauth/authorize', | |
params: { scope: 'openid myinfo.name' }, | |
}, | |
jwks_endpoint: 'https://api.id.gov.sg/v2/.well-known/jwks.json', | |
token: 'https://api.id.gov.sg/v2/oauth/token', | |
userinfo: { | |
url: 'https://api.id.gov.sg/v2/oauth/userinfo', | |
// The result of this method will be the input to the `profile` callback. | |
async request(context) { | |
// context contains useful properties to help you make the request. | |
return await makeUserinfoRequest(context) | |
} | |
}, | |
checks: ['pkce', 'state'], | |
idToken: true, | |
client: { token_endpoint_auth_method: 'client_secret_post' }, | |
clientId: process.env.SGID_CLIENT_ID, | |
clientSecret: process.env.SGID_CLIENT_SECRET, | |
profile: async (profile) => { | |
return { | |
id: profile.sub, | |
name: profile.name, | |
} | |
}, | |
}, | |
], | |
// Persist accounts and session state to Supabase | |
adapter: SupabaseAdapter({ | |
url: process.env.SUPABASE_URL || '', | |
secret: process.env.SUPABASE_SERVICE_ROLE_KEY || '', | |
}) as Adapter, | |
pages: { | |
signIn: '/sign-in', | |
}, | |
session: { | |
// Choose how you want to save the user session. | |
// The default is `"jwt"`, an encrypted JWT (JWE) stored in the session cookie. | |
// If you use an `adapter` however, we default it to `"database"` instead. | |
// You can still force a JWT session by explicitly defining `"jwt"`. | |
// When using `"database"`, the session cookie will only contain a `sessionToken` value, | |
// which is used to look up the session in the database. | |
// strategy: "jwt", | |
// Seconds - How long until an idle session expires and is no longer valid. | |
maxAge: 30 * 24 * 60 * 60, // 30 days | |
// Seconds - Throttle how frequently to write to database to extend a session. | |
// Use it to limit write operations. Set to 0 to always update the database. | |
// Note: This option is ignored if using JSON Web Tokens | |
updateAge: 24 * 60 * 60, // 24 hours | |
}, | |
callbacks: { | |
async jwt({ token, user, account, profile }) { | |
// Persist the OAuth access_token and or the user id to the token right after signin | |
if (account) { | |
token.accessToken = account.access_token | |
token.id = profile?.sub | |
} | |
return token; | |
}, | |
async session({ session, token, user }) { | |
// Send properties to the client, like an access_token from a provider. | |
const signingSecret = process.env.SUPABASE_JWT_SECRET | |
// console.log('Signing Secret:', signingSecret); | |
if (signingSecret) { | |
const payload = { | |
aud: "authenticated", | |
exp: Math.floor(new Date(session.expires).getTime() / 1000), | |
sub: user.id, | |
// email: user.email, | |
role: "authenticated", | |
} | |
session.supabaseAccessToken = jwt.sign(payload, signingSecret) as string; | |
// console.log('New Session:', session); | |
// session.jwt = token.jwt as string; | |
// session.id = token.id as string; | |
} | |
return session; | |
}, | |
} | |
} satisfies NextAuthOptions | |
// Use it in server contexts | |
export async function auth(...args: [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]] | [NextApiRequest, NextApiResponse] | []) { | |
return getServerSession(...args, config) | |
} |