Skip to content

Commit

Permalink
Add /login/token to support customer login API (#1775)
Browse files Browse the repository at this point in the history
* Add /login/token to support Customer Login API

* Add stub for generating Customer Login API tokens
  • Loading branch information
bookernath authored Jan 2, 2025
1 parent 6eb30ac commit 632a645
Show file tree
Hide file tree
Showing 10 changed files with 367 additions and 170 deletions.
5 changes: 5 additions & 0 deletions .changeset/grumpy-falcons-explode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": patch
---

Add stub for generating Customer Login API tokens for SSO integrations
5 changes: 5 additions & 0 deletions .changeset/mighty-candles-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": patch
---

Add /login/token endpoint to power Customer Login API
1 change: 1 addition & 0 deletions core/app/[locale]/(default)/(auth)/login/_actions/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const login = async (formData: FormData): Promise<LoginResponse> => {
const locale = await getLocale();

const credentials = Credentials.parse({
type: 'password',
email: formData.get('email'),
password: formData.get('password'),
});
Expand Down
43 changes: 43 additions & 0 deletions core/app/login/token/[token]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* This route is used to accept customer login token JWTs from the
* [Customer Login API](https://developer.bigcommerce.com/docs/start/authentication/customer-login)
* and log the customers in using alternative authentication methods
*/

import { decodeJwt } from 'jose';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { redirect, unstable_rethrow as rethrow } from 'next/navigation';

import { signIn } from '~/auth';

interface TokenParams {
params: Promise<{ token: string }>;
}

export async function GET(request: Request, { params }: TokenParams) {
const token = (await params).token;

try {
// decode token without checking signature to get redirect path
// token is not checked for validity here, so it could be expired or invalid at this point
// token validity and signature are checked in the signIn function
const claims = decodeJwt(token);
const redirectTo =
typeof claims.redirect_to === 'string' ? claims.redirect_to : '/account/orders';

// sign in with token which will check validity against BigCommerce API
// and redirect to redirectTo
await signIn('credentials', {
type: 'jwt',
jwt: token,
redirectTo,
});
} catch (error) {
rethrow(error);

redirect(`/login?error=InvalidToken`);
}
}

export const runtime = 'edge';
export const dynamic = 'force-dynamic';
156 changes: 0 additions & 156 deletions core/auth.ts

This file was deleted.

60 changes: 60 additions & 0 deletions core/auth/customer-login-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { randomUUID } from 'crypto';
import { SignJWT } from 'jose';

/**
* Build a Customer Login API JWT which can be used in auth/index.ts to log in a customer
* using the LoginWithTokenMutation, or used as a redirect to /login/token/[token]
*
* This is a stub intended to be used when implementing 3rd party authentication callbacks
*
* Requires that BIGCOMMERCE_CLIENT_SECRET and BIGCOMMERCE_CLIENT_ID are set in the environment
* from a client that has the Customer Login scope enabled
*
* @param {number} customerId - The BigCommerce customer ID to generate the login token for
* @param {number} [channelId] - Channel ID that the customer will be logged into
* @param {string} [redirectTo] - Relative URL to redirect to after successful login
* @param {Record<string, any>} [additionalClaims] - Optional additional claims to include in the JWT
* @returns {Promise<string>} A JWT token that can be used to authenticate the customer
* @throws {Error} If BIGCOMMERCE_CLIENT_SECRET is not set in environment variables
* @throws {Error} If BIGCOMMERCE_CLIENT_ID is not set in environment variables
*/
export const generateCustomerLoginApiJwt = async (
customerId: number,
channelId: number,
redirectTo: string = '/account/orders',
additionalClaims?: Record<string, any>,
): Promise<string> => {
const clientId = process.env.BIGCOMMERCE_CLIENT_ID;
const clientSecret = process.env.BIGCOMMERCE_CLIENT_SECRET;
const storeHash = process.env.BIGCOMMERCE_STORE_HASH;

if (!clientSecret) {
throw new Error('BIGCOMMERCE_CLIENT_SECRET is not set in environment variables');
}

if (!clientId) {
throw new Error('BIGCOMMERCE_CLIENT_ID is not set in environment variables');
}

if (!storeHash) {
throw new Error('BIGCOMMERCE_STORE_HASH is not set in environment variables');
}

const payload = {
iss: clientId,
iat: Math.floor(Date.now() / 1000),
jti: randomUUID(),
operation: 'customer_login',
store_hash: storeHash,
customer_id: Math.floor(customerId),
...(channelId && { channel_id: channelId }),
...(redirectTo && { redirect_to: redirectTo }),
...(additionalClaims || {}),
};

// Convert client secret to Uint8Array for jose library
const secretKey = new TextEncoder().encode(clientSecret);

// Create and sign the JWT
return await new SignJWT(payload).setProtectedHeader({ alg: 'HS256', typ: 'JWT' }).sign(secretKey);
};
Loading

0 comments on commit 632a645

Please sign in to comment.