@krakentech/blueprint-auth
This is our package that provides Next.js authentication utilities for the Kraken API.
You can install this package via npm
with an organisation token - please get in touch if you need one.
API reference
withBlueprintAuth
This is a next.config.js webpack
config wrapper to fix 'No QueryClient set, use QueryClientProvider to set one' issue with @tanstack/react-query
when imported in server-side code:
Import
import { withBlueprintAuth } from '@krakentech/blueprint-auth';
Usage
In your Next.js next.config.js
file:
const withBlueprintAuth = require('@krakentech/blueprint-auth');
module.exports = {
(...)
webpack: (config, context) => {
(...)
return withBlueprintAuth({ config, context });
},
};
AuthProvider
Provides context to auth hooks.
Import
import { AuthProvider } from '@krakentech/blueprint-auth';
Usage
In your Next.js _app.tsx
file:
import { AuthProvider } from '@krakentech/blueprint-auth';
function App({ Component, pageProps }) {
return (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
);
}
export default App;
This is a very basic example. You probably have other providers in your
_app.tsx
file, make sure you wrap the ones that need to use the auth hooks with theAuthProvider
.
authMiddleware
This function is meant to be used in a Next.js middleware function:
- It will check if the user is authenticated and has the necessary tokens to access the requested protected route.
- If the user is not authenticated, it will redirect them to the login page.
- If the user is authenticated, it will check if the access token is still valid and refresh it if necessary.
The
krakenConfig.xClientIpSecrectKey
is used as the value of thex-client-ip-authorization
header. This header is used by Kraken to prevent valid server-side requests, which originate from a single/pool of IPs (this will be the case if you host your app in Vercel for example), from being erroneously rate-limited. More information can be found on this Kraken announcement.
Import
import { authMiddleware } from '@krakentech/blueprint-auth';
We expose this function separately to avoid bundling code that is unsupported in Edge Runtime. Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation
Usage
In your Next.js middleware.ts
file:
import { NextResponse, type NextRequest } from 'next/server';
import { authMiddleware } from '@krakentech/blueprint-auth';
export const config = {
matcher: [
'/anon/:path*',
'/dashboard/:path*',
'/login',
'/masquerade/:path*',
],
};
export async function middleware(req: NextRequest) {
const res = NextResponse;
return await authMiddleware({
req,
res,
krakenConfig: {
graphqlEndpoint: process.env.KRAKEN_GRAPHQL_ENDPOINT,
xClientIpSecretKey: process.env.KRAKEN_X_CLIENT_IP_SECRET_KEY,
},
appRoutes: {
anon: {
path: '/anon',
getPreSignedKey: (url) => {
const [_anon, preSignedKey] = url.pathname
.split('/')
.filter(Boolean);
return { preSignedKey };
},
},
dashboard: {
path: '/dashboard',
},
login: {
path: '/login',
},
masquerade: {
path: '/masquerade',
getUserIdAndMasqueradeToken: (url) => {
const [_masquerade, userId, masqueradeToken] = url.pathname
.split('/')
.filter(Boolean);
return { userId, masqueradeToken };
},
},
},
setCustomHeaders: (headers) => {
headers.set(name, value);
},
useSecureCookie: !process.env.NODE_ENV === 'test',
});
}
loginHandler
Handles the login process. This function must be used in a Next.js API route:
- It first checks if the request method is
POST
, if not, it returns405
response. - Then it creates a new
Headers
object, optionally including any custom headers passed in. - If the request is successful, it sets the necessary auth cookies necessary and returns a
200
response with a standardised JSON object. - If the request fails, it returns a
400
response with a standardised JSON object.
Import
import { loginHandler } from '@krakentech/blueprint-auth';
Usage
In your Next.js login api route:
import type { NextApiRequest, NextApiResponse } from 'next';
import { loginHandler } from '@krakentech/blueprint-auth';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
await loginHandler({
req,
res,
krakenConfig: {
graphqlEndpoint: process.env.KRAKEN_GRAPHQL_ENDPOINT,
xClientIpSecretKey: process.env.KRAKEN_X_CLIENT_IP_SECRET_KEY,
},
setCustomHeaders: (headers) => {
headers.set(name, value);
},
useSecureCookie: !process.env.NODE_ENV === 'test',
});
}
logoutHandler
Handles the logout process. This function must be used in a Next.js API route:
- It first checks if the request method is
GET
, if not, it returns405
response. - Then it removes every auth related cookie and returns a
200
response with theCache-Control
header set and a standardised JSON object.
Import
import { logoutHandler } from '@krakentech/blueprint-auth';
Usage
In your Next.js logout api route:
import type { NextApiRequest, NextApiResponse } from 'next';
import { logoutHandler } from '@krakentech/blueprint-auth';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
logoutHandler({
req,
res,
});
}
sessionHandler
Handles the session state. This function must be used in a Next.js API route:
- It first checks if the request method is
GET
, if not, it returns405
response. - Then asserts the current auth state of the user by retrieving the auth related cookies.
- The
isAuthenticated
value is derived from the presence of either:- both 'access' and 'refresh' cookies
- a 'masquerade' cookie
- The
isMasquerading
value is derived from the presence of a 'masquerade' cookie. - The
sub
state is derived from the presence of a 'sub' cookie.
- The
- Then returns a
200
response with theCache-Control
header set to prevent caching and a JSON object with the derived state.
Import
import { sessionHandler } from '@krakentech/blueprint-auth';
Usage
In your Next.js session api route:
import type { NextApiRequest, NextApiResponse } from 'next';
import { sessionHandler } from '@krakentech/blueprint-auth';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
sessionHandler({
req,
res,
});
}
graphqlHandler
Handles every Kraken GraphQL request. This function must be used in a Next.js API route:
- It first checks if the request method is
POST
, if not, it returns405
response. - Then it creates a new
Headers
object, optionally including any custom headers passed in. - If the request is successful, it returns a
200
response with the data object. - If the request fails, it returns a
200
response with the error object. - If the request fails due to a
ACCESS TOKEN EXPIRED
error, it will attempt to refresh the access token and retry the request. - If the request fails due to other auth related errors, it will remove the auth cookies and return the error.
If you intend to make organization scoped requests (either by using the getGraphQLClient
helper with it's scope
prop set to organization
or by using the organizationScopedGraphQLClient
returned by the useGraphQLClients
hook) you need to pass a few additional props:
krakenConfig.organizationSecretKey
: the secret key of the organization.encryption
: an object with thekey
andiv
values used to encrypt theorganization
token. We use these to encrypt/decrypt theorganization
token. This is to prevent the token from being exposed to the client as this is a sensitive and unique value issued by Kraken and a bad actor could potentially use it to make organization scoped authenticated queries/mutations. Both thekey
andiv
must be at least 12 characters long. One possible way of generating them is by using the following command:openssl rand -base64 12
. We recommend storing these values in your environment variables. Both of them should be rotated periodically.edgeConfig
: an object with theenvVar
,teamId
andauthToken
values. We are storing the encryptedorganization
token in the Vercel Edge Config Store connected to you Vercel project, which means that you need to set one up. You can do that at the following URL:https://vercel.com/<team-id>/<your-project-name>/stores
. Once you create one, Vercel will automatically create anEDGE_CONFIG
environment variable for you. This is the value that you need to pass to theedgeConfig.envVar
prop. Its value should look like this:https://edge-config.vercel.com/<edge-config-store-id>?token=<auth-token>
. You'll also have to create an access token so that you can interact with the store.
If you try to make an organization scoped request without passing these props, the request will fail with errors stating that they are missing. Basically, if you see any of the following errors, it means that you need to pass the missing props to the graphqlHandler
function:
- blueprint-auth: getOrganizationAccessToken — krakenConfig.organizationSecretKey is required.
- blueprint-auth: createOrganizationTokenUtils — edgeConfig.envVar, edgeConfig.teamId, and edgeConfig.authToken are required.
- blueprint-auth: createEncryptionUtils — encryption.key and encryption.iv are required. Bear in mind that their values should have at least 12 characters. One possible way of generating them is by using the following command: openssl rand -base64 12.
Import
import { graphqlHandler } from '@krakentech/blueprint-auth';
Usage
In your Next.js graphql api route:
import type { NextApiRequest, NextApiResponse } from 'next';
import { graphqlHandler } from '@krakentech/blueprint-auth';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
await graphqlHandler({
req,
res,
krakenConfig: {
graphqlEndpoint: process.env.KRAKEN_GRAPHQL_ENDPOINT,
organizationSecretKey: process.env.KRAKEN_ORGANIZATION_KEY,
xClientIpSecretKey: process.env.KRAKEN_X_CLIENT_IP_SECRET_KEY,
},
setCustomHeaders: (headers) => {
headers.set(name, value);
},
useSecureCookie: !process.env.NODE_ENV === 'test',
shouldValidateUrl: !process.env.NODE_ENV === 'test',
encryption: {
key: process.env.AUTH_ENCRYPTION_KEY,
iv: process.env.AUTH_ENCRYPTION_IV,
},
edgeConfig: {
envVar: process.env.EDGE_CONFIG,
teamId: process.env.VERCEL_TEAM_ID,
authToken: process.env.VERCEL_AUTH_TOKEN,
},
});
}
krakenOAuthHandler
Handles the OAuth process. This function must be used in a Next.js API route:
Import
import { krakenOAuthHandler } from '@krakentech/blueprint-auth';
Usage
In your Next.js OAuth api route:
import type { NextApiRequest, NextApiResponse } from 'next';
import { krakenOAuthHandler } from '@krakentech/blueprint-auth';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
await krakenOAuthHandler({
req,
res,
krakenConfig: {
authEndpoint: process.env.KRAKEN_AUTH_ENDPOINT,
clientId: process.env.KRAKEN_CLIENT_ID,
clientSecret: process.env.KRAKEN_CLIENT_SECRET,
},
appRoutes: {
login: '/login',
logout: '/logout',
krakenOAuth: '/api/auth/kraken-oauth',
},
});
}
updateOrgTokenHandler
Handles the update organization token request. This is meant to be a cron job and must be used in a Next.js API route:
- It first checks if the request is authorized by checking if the authorization header matches the
CRON_SECRET
environment variable. The secret can be any string value. - Then it updates the organization token.
- If both the request to get the token and write the token to the edge config are successful, it returns a
200
response with the data object. - If the request fails, it returns a
200
response with the error object.
You will also have to configure the api route where you use this handler to be a cron job in Vercel.
You can do that by creating a vercel.json
file at the root of your project with the following content:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"crons": [
{
"path": "/api/auth/update-org-token",
"schedule": "*/55 * * * *"
}
]
}
Set the
schedule
value to the desired cron job schedule. The example above will run the cron job every 55 minutes. (usually, the organization token expires every hour)
Import
import { updateOrgTokenHandler } from '@krakentech/blueprint-auth';
Usage
In your Next.js update organization token api route:
import type { NextApiRequest, NextApiResponse } from 'next';
import { updateOrgTokenHandler } from '@krakentech/blueprint-auth';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
await updateOrgTokenHandler({
req,
res,
krakenConfig: {
graphqlEndpoint: process.env.KRAKEN_GRAPHQL_ENDPOINT,
organizationSecretKey: process.env.KRAKEN_ORGANIZATION_KEY,
xClientIpSecretKey: process.env.KRAKEN_X_CLIENT_IP_SECRET_KEY,
},
encryption: {
key: process.env.AUTH_ENCRYPTION_KEY,
iv: process.env.AUTH_ENCRYPTION_IV,
},
edgeConfig: {
envVar: process.env.EDGE_CONFIG,
teamId: process.env.VERCEL_TEAM_ID,
authToken: process.env.VERCEL_AUTH_TOKEN,
},
});
}
createKrakenOAuthURI
Creates the Kraken OAuth URI.
Import
import { createKrakenOAuthURI } from '@krakentech/blueprint-auth';
Usage
In your Next.js page where you want to redirect the user to the Kraken OAuth page. This URI should be used in a button or link to redirect the user to the Kraken OAuth page.
import { createKrakenOAuthURI } from '@krakentech/blueprint-auth';
...
export const getServerSideProps: GetServerSideProps = async (context) => {
const { req, res } = context;
const authorizeURI = await createKrakenOAuthURI({
req,
res,
krakenConfig: {
authEndpoint: process.env.KRAKEN_AUTH_ENDPOINT as string,
oauthClientId: process.env.KRAKEN_OAUTH_CLIENT_ID as string,
},
krakenOAuthApiRoute: '/api/auth/kraken-oauth',
});
return {
props: {
authorizeURI,
},
};
};
useLogin
Hook that provides a function to login a user.
Import
import { useLogin } from '@krakentech/blueprint-auth';
Usage
In your component:
import { useLogin } from '@krakentech/blueprint-auth';
const LoginButton = ({ email, password }) => {
const { mutate } = useLogin();
return <button onClick={() => mutate({ email, password })}>Login</button>;
};
useLogout
Hook that provides a function to logout a user.
Import
import { useLogout } from '@krakentech/blueprint-auth';
Usage
In your component:
import { useLogout } from '@krakentech/blueprint-auth';
const LogoutButton = () => {
const { mutate } = useLogout();
return <button onClick={() => mutate()}>Logout</button>;
};
useSession
Hook that provides the current auth session data. Bear in mind that this "@tanstack/react-query"
hook has all refetch properties set to true
( refetchOnMount
, refetchOnWindowFocus
and refetchOnReconnect
) which means that you should see quite a few requests being made to the session endpoint, especially if you keep refocusing the window when using the browser's developer tools.
Import
import { useSession } from '@krakentech/blueprint-auth';
Usage
In your component:
import { useSession } from '@krakentech/blueprint-auth';
const Page = () => {
const {
isInitialLoading,
isAuthenticated,
isMasquerading,
sub,
invalidateSessionCache,
} = useSession();
if (isInitialLoading) {
return <p>Loading...</p>;
}
return (
<div>
<p>Authenticated: {isAuthenticated ? 'Yes' : 'No'}</p>
<p>Masquerading: {isMasquerading ? 'Yes' : 'No'}</p>
<p>Sub: {sub}</p>
<button onClick={() => invalidateSessionCache()}>
Invalidate session
</button>
</div>
);
};
useKrakenAuthErrorHandler
There are two methods available in this hook:
getAuthErrorCode(e: unknown)
- this method returns theerrorCode
based on theerrorType
(such as 'UNAUTHORIZED', 'AUTH_HEADER_NOT_PROVIDED', etc) from the server response.handleKrakenErrors(onError?: (e: unknown) => void))
- this method can be used in theerrorCallback
prop in the Blueprint hooks and in other places where you need to handle the authentication related errors. It will check the error code and either call the custom onError function if provided, or redirect the user to the login page with the current page as the next page and the error code as a query parameter
Import
import { useKrakenAuthErrorHandler } from '@krakentech/blueprint-auth';
Usage
In your component:
import { useKrakenAuthErrorHandler } from "@krakentech/blueprint-auth";
...
// Check the error code using the error received from the server. If it's authentication related error, you will get the error code.
const { getAuthErrorCode } = useKrakenAuthErrorHandler();
...
const errorCode = getAuthErrorCode({ response: { errors: error.graphQLErrors }});
if (errorCode) {
//Do something
}
// Pass it in the errorCallback in the Blueprint hooks. Example with useMeasurements hook below
const { handleKrakenErrors } = useKrakenAuthErrorHandler();
...
const measurements = useMeasurements({
variables: {
accountNumber,
propertyId,
utilityType,
dateLocale: config.i18n.dateLocales[locale || ''],
marketSupplyPointId: prm.id,
enabled: true,
startAt: STATS_START_AT,
endAt: STATS_ANCHOR_DATE,
first: 14,
dateRange: DateRangeToggleValue.WEEK,
},
errorCallback: handleKrakenErrors,
locale,
});
getServerSideGraphQLClients
This helper function returns GraphQL clients either scoped to the user or the organization to make it easier for you to make authenticated GraphQL requests.
It's meant to be used in getServerSideProps
or getStaticProps
functions in Next.js pages.
Bear in mind that, if you enabled Vercel's deployment protection, you will have to also enable the "Protection Bypass for Automation" so that requests that use either of these functions can be made successfully in preview deployments as well.
CAVEAT - Requests made with this client in
getStaticProps
won't be authenticated if the scope is set touser
. This is becausegetStaticProps
runs at build time and doesn't have access to the session's cookies.
Import
import { getServerSideGraphQLClients } from '@krakentech/blueprint-auth';
Usage
In your Next.js page:
import { getServerSideGraphQLClients } from '@krakentech/blueprint-auth';
import { getVercelOrigin } from '@krakentech/blueprint-utils/environment'; // we recommend using this utility if you're hosting on Vercel
...
const ViewerQuery = gql`
query Viewer {
viewer {
preferredName
email
accounts {
number
}
}
}
`;
const CreateAccountMutation = gql`
mutation CreateAccount($input: CreateAccountInput!) {
createAccount(input: $input) {
accountNumber
token
userId
}
}
`;
export const getServerSideProps: GetServerSideProps = async (context) => {
const { userScopedGraphQLClient, organizationScopedGraphQLClient } = getServerSideGraphQLClients({
graphqlApiRoute: `${getVercelOrigin()}/api/graphql`,
context,
});
const viewer = await userScopedGraphQLClient.request(ViewerQuery);
const createdAccount = await organizationScopedGraphQLClient.request(CreateAccountMutation, {
input: {
...
},
});
return {
props: {
...,
},
};
};
useGraphQLClients
This hook returns an object with two GraphQL clients: one scoped to the user and the other to the organization.
Import
import { useGraphQLClients } from '@krakentech/blueprint-auth';
Usage
In a custom react query hook, for example:
import { useMutation } from 'react-query';
import { getGraphQLClient } from '@krakentech/blueprint-auth';
const UpdatePassword = gql`
mutation UpdatePassword($input: UpdatePasswordInput!) {
updatePassword(input: $input) {
viewer {
givenName
familyName
mobile
email
}
}
}
`;
export const useCreateAccount = (input: UpdatePasswordInput) => {
const { userScopedGraphQLClient } = useGraphQLClients();
return useMutation({
mutationFn: async () => {
return userScopedGraphQLClient.request(UpdatePassword, {
input,
});
},
});
};
import { useMutation } from 'react-query';
import { getGraphQLClient } from '@krakentech/blueprint-auth';
const CreateAccountMutation = graphql(`
mutation CreateAccount($input: CreateAccountInput!) {
createAccount(input: $input) {
accountNumber
token
userId
}
}
`);
export const useCreateAccount = (input: CreateAccountInput) => {
const { organizationScopedGraphQLClient } = useGraphQLClients();
return useMutation({
mutationFn: async () => {
return organizationScopedGraphQLClient.request(
CreateAccountMutation,
{
input,
}
);
},
});
};
setNextPageSearchParam
This function sets the nextPage
search param in the URL. This is the search param that we are setting in the authMiddleware
function when we redirect the user to the login page.
Import
import { setNextPageSearchParam } from '@krakentech/blueprint-auth';
Usage
import { setNextPageSearchParam } from '@krakentech/blueprint-auth';
const url = new URL(...);
const nextUrl: NextUrl = ...;
setNextPageSearchParam({ url, nextUrl });
redirectToNextPage
This function redirects the user to the nextPage
search param in the URL. It sanitizes the URL to prevent XSS attacks by checking if the URL is either a relative or an absolute path.
Import
import { redirectToNextPage } from '@krakentech/blueprint-auth';
Usage
In your Next.js page:
import { redirectToNextPage } from '@krakentech/blueprint-auth';
...
const Page = () => {
const router = useRouter();
useEffect(() => {
redirectToNextPage({
router,
defaultPath: '/dashboard',
});
}, []);
return <p>Redirecting...</p>;
};