Merge pull request #8589 from backstage/feature/add-gcp-iap-auth-provider

Updated: add gcp iap auth provider
This commit is contained in:
Fredrik Adelöw
2022-01-03 13:04:48 +01:00
committed by GitHub
15 changed files with 621 additions and 32 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Added Google Cloud Identity-Aware Proxy as an identity provider.
+32 -6
View File
@@ -9,6 +9,7 @@ import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import express from 'express';
import { JsonValue } from '@backstage/types';
import { JSONWebKey } from 'jose';
import { Logger as Logger_2 } from 'winston';
import { PluginDatabaseManager } from '@backstage/backend-common';
@@ -59,8 +60,8 @@ export type Auth0ProviderOptions = {
};
// @public
export type AuthHandler<AuthResult> = (
input: AuthResult,
export type AuthHandler<TAuthResult> = (
input: TAuthResult,
) => Promise<AuthHandlerResult>;
// @public
@@ -249,6 +250,11 @@ export const createBitbucketProvider: (
options?: BitbucketProviderOptions | undefined,
) => AuthProviderFactory;
// @public
export function createGcpIapProvider(
options: GcpIapProviderOptions,
): AuthProviderFactory;
// Warning: (ae-missing-release-tag) "createGithubProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -335,6 +341,26 @@ export const encodeState: (state: OAuthState) => string;
// @public (undocumented)
export const ensuresXRequestedWith: (req: express.Request) => boolean;
// @public
export type GcpIapProviderOptions = {
authHandler?: AuthHandler<GcpIapResult>;
signIn: {
resolver: SignInResolver<GcpIapResult>;
};
};
// @public
export type GcpIapResult = {
iapToken: GcpIapTokenInfo;
};
// @public
export type GcpIapTokenInfo = {
sub: string;
email: string;
[key: string]: JsonValue;
};
// Warning: (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "getEntityClaims" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -661,14 +687,14 @@ export type SamlProviderOptions = {
};
// @public
export type SignInInfo<AuthResult> = {
export type SignInInfo<TAuthResult> = {
profile: ProfileInfo;
result: AuthResult;
result: TAuthResult;
};
// @public
export type SignInResolver<AuthResult> = (
info: SignInInfo<AuthResult>,
export type SignInResolver<TAuthResult> = (
info: SignInInfo<TAuthResult>,
context: {
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
+4 -1
View File
@@ -36,6 +36,7 @@
"@backstage/config": "^0.1.11",
"@backstage/errors": "^0.1.5",
"@backstage/test-utils": "^0.2.1",
"@backstage/types": "^0.1.1",
"@google-cloud/firestore": "^4.15.1",
"@types/express": "^4.17.6",
"@types/passport": "^1.0.3",
@@ -46,6 +47,7 @@
"express-promise-router": "^4.1.0",
"express-session": "^1.17.1",
"fs-extra": "9.1.0",
"google-auth-library": "^7.6.1",
"helmet": "^4.0.0",
"jose": "^1.27.1",
"jwt-decode": "^3.1.0",
@@ -83,7 +85,8 @@
"@types/passport-saml": "^1.1.3",
"@types/passport-strategy": "^0.2.35",
"@types/xml2js": "^0.4.7",
"msw": "^0.35.0"
"msw": "^0.35.0",
"supertest": "^6.1.3"
},
"files": [
"dist",
@@ -13,5 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { createAwsAlbProvider } from './provider';
export type { AwsAlbProviderOptions } from './provider';
@@ -18,7 +18,7 @@ import express from 'express';
import { JWT } from 'jose';
import {
ALB_ACCESSTOKEN_HEADER,
ALB_ACCESS_TOKEN_HEADER,
ALB_JWT_HEADER,
AwsAlbAuthProvider,
} from './provider';
@@ -80,7 +80,7 @@ describe('AwsAlbAuthProvider', () => {
header: jest.fn(name => {
if (name === ALB_JWT_HEADER) {
return mockJwt;
} else if (name === ALB_ACCESSTOKEN_HEADER) {
} else if (name === ALB_ACCESS_TOKEN_HEADER) {
return mockAccessToken;
}
return undefined;
@@ -88,7 +88,7 @@ describe('AwsAlbAuthProvider', () => {
} as unknown as express.Request;
const mockRequestWithoutJwt = {
header: jest.fn(name => {
if (name === ALB_ACCESSTOKEN_HEADER) {
if (name === ALB_ACCESS_TOKEN_HEADER) {
return mockAccessToken;
}
return undefined;
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
AuthHandler,
AuthProviderFactory,
@@ -35,7 +36,7 @@ import { AuthenticationError } from '@backstage/errors';
import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
export const ALB_JWT_HEADER = 'x-amzn-oidc-data';
export const ALB_ACCESSTOKEN_HEADER = 'x-amzn-oidc-accesstoken';
export const ALB_ACCESS_TOKEN_HEADER = 'x-amzn-oidc-accesstoken';
type Options = {
region: string;
@@ -134,7 +135,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
private async getResult(req: express.Request): Promise<AwsAlbResult> {
const jwt = req.header(ALB_JWT_HEADER);
const accessToken = req.header(ALB_ACCESSTOKEN_HEADER);
const accessToken = req.header(ALB_ACCESS_TOKEN_HEADER);
if (jwt === undefined) {
throw new AuthenticationError(
@@ -144,7 +145,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
if (accessToken === undefined) {
throw new AuthenticationError(
`Missing ALB OIDC header: ${ALB_ACCESSTOKEN_HEADER}`,
`Missing ALB OIDC header: ${ALB_ACCESS_TOKEN_HEADER}`,
);
}
@@ -0,0 +1,134 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConflictError } from '@backstage/errors';
import { OAuth2Client } from 'google-auth-library';
import { createTokenValidator, parseRequestToken } from './helpers';
const validJwt =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzcyI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.T2BNS4G-6RoiFnXc8Q8TiwdWzTpNitY8jcsGM3N3-Yo';
beforeEach(() => {
jest.clearAllMocks();
});
describe('helpers', () => {
describe('createTokenValidator', () => {
it('runs the happy path', async () => {
const mockClient = {
getIapPublicKeys: async () => ({ pubkeys: '' }),
verifySignedJwtWithCertsAsync: async () => ({
getPayload: () => ({ sub: 's', email: 'e@mail.com' }),
}),
};
const validator = createTokenValidator(
'a',
mockClient as unknown as OAuth2Client,
);
await expect(validator(validJwt)).resolves.toMatchObject({
sub: 's',
email: 'e@mail.com',
});
});
it('throws if the client throws', async () => {
const mockClient = {
getIapPublicKeys: async () => {
throw new TypeError('bam');
},
};
const validator = createTokenValidator(
'a',
mockClient as unknown as OAuth2Client,
);
await expect(validator(validJwt)).rejects.toThrowError(TypeError);
});
it('rejects empty payload', async () => {
const mockClient = {
getIapPublicKeys: async () => ({ pubkeys: '' }),
verifySignedJwtWithCertsAsync: async () => ({
getPayload: () => undefined,
}),
};
const validator = createTokenValidator(
'a',
mockClient as unknown as OAuth2Client,
);
await expect(validator(validJwt)).rejects.toMatchObject({
name: 'TypeError',
message: 'Token had no payload',
});
});
});
describe('parseRequestToken', () => {
it('runs the happy path', async () => {
await expect(
parseRequestToken(
validJwt,
async () => ({ sub: 's', email: 'e@mail.com' } as any),
),
).resolves.toMatchObject({
iapToken: {
sub: 's',
email: 'e@mail.com',
},
});
});
it('rejects bad tokens', async () => {
await expect(
parseRequestToken(7, undefined as any),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Missing Google IAP header: x-goog-iap-jwt-assertion',
});
await expect(
parseRequestToken(undefined, undefined as any),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Missing Google IAP header: x-goog-iap-jwt-assertion',
});
await expect(
parseRequestToken('', undefined as any),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Missing Google IAP header: x-goog-iap-jwt-assertion',
});
});
it('translates validator errors', async () => {
await expect(
parseRequestToken(validJwt, async () => {
throw new ConflictError('Ouch');
}),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Google IAP token verification failed, ConflictError: Ouch',
});
});
it('rejects bad token payloads', async () => {
await expect(
parseRequestToken(validJwt, async () => ({ sub: 'a' } as any)),
).rejects.toMatchObject({
name: 'AuthenticationError',
message: 'Google IAP token payload is missing sub and/or email claim',
});
});
});
});
@@ -0,0 +1,84 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AuthenticationError } from '@backstage/errors';
import { OAuth2Client, TokenPayload } from 'google-auth-library';
import { AuthHandler } from '../types';
import { GcpIapResult, IAP_JWT_HEADER } from './types';
export function createTokenValidator(
audience: string,
mockClient?: OAuth2Client,
): (token: string) => Promise<TokenPayload> {
const client = mockClient ?? new OAuth2Client();
return async function tokenValidator(token) {
// TODO(freben): Rate limit the public key reads. It may be sensible to
// cache these for some reasonable time rather than asking for the public
// keys on every single sign-in. But since the rate of events here is so
// slow, I decided to keep it simple for now.
const response = await client.getIapPublicKeys();
const ticket = await client.verifySignedJwtWithCertsAsync(
token,
response.pubkeys,
audience,
['https://cloud.google.com/iap'],
);
const payload = ticket.getPayload();
if (!payload) {
throw new TypeError('Token had no payload');
}
return payload;
};
}
export async function parseRequestToken(
jwtToken: unknown,
tokenValidator: (token: string) => Promise<TokenPayload>,
): Promise<GcpIapResult> {
if (typeof jwtToken !== 'string' || !jwtToken) {
throw new AuthenticationError(
`Missing Google IAP header: ${IAP_JWT_HEADER}`,
);
}
let payload: TokenPayload;
try {
payload = await tokenValidator(jwtToken);
} catch (e) {
throw new AuthenticationError(`Google IAP token verification failed, ${e}`);
}
if (!payload.sub || !payload.email) {
throw new AuthenticationError(
'Google IAP token payload is missing sub and/or email claim',
);
}
return {
iapToken: {
...payload,
sub: payload.sub,
email: payload.email,
},
};
}
export const defaultAuthHandler: AuthHandler<GcpIapResult> = async ({
iapToken,
}) => ({ profile: { email: iapToken.email } });
@@ -0,0 +1,22 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { createGcpIapProvider } from './provider';
export type {
GcpIapProviderOptions,
GcpIapResult,
GcpIapTokenInfo,
} from './types';
@@ -0,0 +1,76 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import express from 'express';
import request from 'supertest';
import { GcpIapProvider } from './provider';
beforeEach(() => {
jest.clearAllMocks();
});
describe('GcpIapProvider', () => {
const authHandler = jest.fn();
const signInResolver = jest.fn();
const tokenValidator = jest.fn();
const logger = getVoidLogger();
it('runs the happy path', async () => {
const provider = new GcpIapProvider({
authHandler,
signInResolver,
tokenValidator,
tokenIssuer: {} as any,
catalogIdentityClient: {} as any,
logger,
});
// { "sub": "user:default/me", "ent": ["group:default/home"] }
const backstageToken =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvbWUiLCJlbnQiOlsiZ3JvdXA6ZGVmYXVsdC9ob21lIl19.CbmAKzFErGmtsnpRxyPc7dHv7WEjb5lY6206YCzR_Rc';
const iapToken = { sub: 's', email: 'e@mail.com' };
authHandler.mockResolvedValueOnce({ email: 'e@mail.com' });
signInResolver.mockResolvedValueOnce({ id: 'i', token: backstageToken });
tokenValidator.mockResolvedValueOnce(iapToken);
const app = express();
app.use('/refresh', provider.refresh.bind(provider));
const response = await request(app)
.get('/refresh')
.set('x-goog-iap-jwt-assertion', 'token');
expect(response.status).toBe(200);
expect(response.get('content-type')).toBe(
'application/json; charset=utf-8',
);
expect(response.body).toEqual({
backstageIdentity: {
id: 'i',
idToken: backstageToken,
token: backstageToken,
identity: {
type: 'user',
userEntityRef: 'user:default/me',
ownershipEntityRefs: ['group:default/home'],
},
},
providerInfo: { iapToken },
});
});
});
@@ -0,0 +1,125 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
import { TokenPayload } from 'google-auth-library';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity/types';
import { CatalogIdentityClient } from '../../lib/catalog';
import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
import {
AuthHandler,
AuthProviderFactory,
AuthProviderRouteHandlers,
SignInResolver,
} from '../types';
import {
createTokenValidator,
defaultAuthHandler,
parseRequestToken,
} from './helpers';
import {
GcpIapProviderOptions,
GcpIapResponse,
GcpIapResult,
IAP_JWT_HEADER,
} from './types';
export class GcpIapProvider implements AuthProviderRouteHandlers {
private readonly authHandler: AuthHandler<GcpIapResult>;
private readonly signInResolver: SignInResolver<GcpIapResult>;
private readonly tokenValidator: (token: string) => Promise<TokenPayload>;
private readonly tokenIssuer: TokenIssuer;
private readonly catalogIdentityClient: CatalogIdentityClient;
private readonly logger: Logger;
constructor(options: {
authHandler: AuthHandler<GcpIapResult>;
signInResolver: SignInResolver<GcpIapResult>;
tokenValidator: (token: string) => Promise<TokenPayload>;
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
logger: Logger;
}) {
this.authHandler = options.authHandler;
this.signInResolver = options.signInResolver;
this.tokenValidator = options.tokenValidator;
this.tokenIssuer = options.tokenIssuer;
this.catalogIdentityClient = options.catalogIdentityClient;
this.logger = options.logger;
}
async start() {}
async frameHandler() {}
async refresh(req: express.Request, res: express.Response): Promise<void> {
const result = await parseRequestToken(
req.header(IAP_JWT_HEADER),
this.tokenValidator,
);
const { profile } = await this.authHandler(result);
const backstageIdentity = await this.signInResolver(
{ profile, result },
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
);
const response: GcpIapResponse = {
providerInfo: { iapToken: result.iapToken },
profile,
backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity),
};
res.json(response);
}
}
/**
* Creates an auth provider for Google Identity-Aware Proxy.
*
* @public
*/
export function createGcpIapProvider(
options: GcpIapProviderOptions,
): AuthProviderFactory {
return ({ config, tokenIssuer, catalogApi, logger }) => {
const audience = config.getString('audience');
const authHandler = options.authHandler ?? defaultAuthHandler;
const signInResolver = options.signIn.resolver;
const tokenValidator = createTokenValidator(audience);
const catalogIdentityClient = new CatalogIdentityClient({
catalogApi,
tokenIssuer,
});
return new GcpIapProvider({
authHandler,
signInResolver,
tokenValidator,
tokenIssuer,
catalogIdentityClient,
logger,
});
};
}
@@ -0,0 +1,96 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JsonValue } from '@backstage/types';
import { AuthHandler, AuthResponse, SignInResolver } from '../types';
/**
* The header name used by the IAP.
*/
export const IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion';
/**
* The data extracted from an IAP token.
*
* @public
*/
export type GcpIapTokenInfo = {
/**
* The unique, stable identifier for the user.
*/
sub: string;
/**
* User email address.
*/
email: string;
/**
* Other fields.
*/
[key: string]: JsonValue;
};
/**
* The result of the initial auth challenge. This is the input to the auth
* callbacks.
*
* @public
*/
export type GcpIapResult = {
/**
* The data extracted from the IAP token header.
*/
iapToken: GcpIapTokenInfo;
};
/**
* The provider info to return to the frontend.
*/
export type GcpIapProviderInfo = {
/**
* The data extracted from the IAP token header.
*/
iapToken: GcpIapTokenInfo;
};
/**
* The shape of the response to return to callers.
*/
export type GcpIapResponse = AuthResponse<GcpIapProviderInfo>;
/**
* Options for {@link createGcpIapProvider}.
*
* @public
*/
export type GcpIapProviderOptions = {
/**
* The profile transformation function used to verify and convert the auth
* response into the profile that will be presented to the user. The default
* implementation just provides the authenticated email that the IAP
* presented.
*/
authHandler?: AuthHandler<GcpIapResult>;
/**
* Configures sign-in for this provider.
*/
signIn: {
/**
* Maps an auth result to a Backstage identity for the user.
*/
resolver: SignInResolver<GcpIapResult>;
};
};
@@ -27,6 +27,7 @@ export * from './oidc';
export * from './okta';
export * from './onelogin';
export * from './saml';
export * from './gcp-iap';
export { factories as defaultAuthProviderFactories } from './factories';
@@ -22,7 +22,9 @@ function parseJwtPayload(token: string) {
}
/**
* Parses token and decorates the BackstageIdentityResponse with identity information sourced from the token
* Parses a Backstage-issued token and decorates the
* {@link BackstageIdentityResponse} with identity information sourced from the
* token.
*
* @public
*/
+31 -18
View File
@@ -200,13 +200,16 @@ export interface BackstageSignInResult {
/**
* The old exported symbol for {@link BackstageSignInResult}.
*
* @public
* @deprecated Use the `BackstageSignInResult` type instead.
* @deprecated Use the {@link BackstageSignInResult} instead.
*/
export type BackstageIdentity = BackstageSignInResult;
/**
* Response object containing the {@link BackstageUserIdentity} and the token from the authentication provider.
* Response object containing the {@link BackstageUserIdentity} and the token
* from the authentication provider.
*
* @public
*/
export interface BackstageIdentityResponse extends BackstageSignInResult {
@@ -220,7 +223,8 @@ export interface BackstageIdentityResponse extends BackstageSignInResult {
* Used to display login information to user, i.e. sidebar popup.
*
* It is also temporarily used as the profile of the signed-in user's Backstage
* identity, but we want to replace that with data from identity and/org catalog service
* identity, but we want to replace that with data from identity and/org catalog
* service
*
* @public
*/
@@ -241,28 +245,32 @@ export type ProfileInfo = {
};
/**
* type of sign in information context, includes the profile information and authentication result which contains auth. related information
* Type of sign in information context. Includes the profile information and
* authentication result which contains auth related information.
*
* @public
*/
export type SignInInfo<AuthResult> = {
export type SignInInfo<TAuthResult> = {
/**
* The simple profile passed down for use in the frontend.
*/
profile: ProfileInfo;
/**
* The authentication result that was received from the authentication provider.
* The authentication result that was received from the authentication
* provider.
*/
result: AuthResult;
result: TAuthResult;
};
/**
* Sign in resolver type describes the function which handles the result of a successful authentication
* and it must return a valid {@link BackstageSignInResult}
* Describes the function which handles the result of a successful
* authentication. Must return a valid {@link BackstageSignInResult}.
*
* @public
*/
export type SignInResolver<AuthResult> = (
info: SignInInfo<AuthResult>,
export type SignInResolver<TAuthResult> = (
info: SignInInfo<TAuthResult>,
context: {
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
@@ -271,23 +279,28 @@ export type SignInResolver<AuthResult> = (
) => Promise<BackstageSignInResult>;
/**
* The return type of authentication handler which must contain a valid profile information
* The return type of an authentication handler. Must contain valid profile
* information.
*
* @public
*/
export type AuthHandlerResult = { profile: ProfileInfo };
/**
* The AuthHandler function is called every time the user authenticates using the provider.
* The AuthHandler function is called every time the user authenticates using
* the provider.
*
* The handler should return a profile that represents the session for the user in the frontend.
* The handler should return a profile that represents the session for the user
* in the frontend.
*
* Throwing an error in the function will cause the authentication to fail, making it
* possible to use this function as a way to limit access to a certain group of users.
* Throwing an error in the function will cause the authentication to fail,
* making it possible to use this function as a way to limit access to a certain
* group of users.
*
* @public
*/
export type AuthHandler<AuthResult> = (
input: AuthResult,
export type AuthHandler<TAuthResult> = (
input: TAuthResult,
) => Promise<AuthHandlerResult>;
export type StateEncoder = (