feat(frontend/auth): Added way to automatically fetch providers
This allows the frontend to list all available providers without hardcoding them in.
This commit is contained in:
parent
9ce9fa44e4
commit
6d630fee92
22 changed files with 1448 additions and 221 deletions
|
|
@ -1,186 +1 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"providers": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^(.*)$": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"auth_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"info_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_secret": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"description": "Secret is stored in the env var",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"inline": {
|
||||
"description": "Secret is inline here",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"inline"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"redirect_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"default": {
|
||||
"unique_id": "email",
|
||||
"name": "name"
|
||||
},
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"unique_id": {
|
||||
"description": "A unique identifier for this provider",
|
||||
"default": "email",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "A name for this provider",
|
||||
"default": "name",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"unique_id",
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"token_url",
|
||||
"auth_url",
|
||||
"info_url",
|
||||
"client_id",
|
||||
"client_secret",
|
||||
"scopes",
|
||||
"redirect_url",
|
||||
"user"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"openid_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_secret": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"description": "Secret is stored in the env var",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"inline": {
|
||||
"description": "Secret is inline here",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"inline"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"redirect_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"default": {
|
||||
"unique_id": "email",
|
||||
"name": "name"
|
||||
},
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"unique_id": {
|
||||
"description": "A unique identifier for this provider",
|
||||
"default": "email",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "A name for this provider",
|
||||
"default": "name",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"unique_id",
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"openid_url",
|
||||
"client_id",
|
||||
"client_secret",
|
||||
"scopes",
|
||||
"redirect_url",
|
||||
"user"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"providers"
|
||||
]
|
||||
}
|
||||
{"type":"object","required":["providers"],"properties":{"providers":{"type":"object","patternProperties":{"^.*$":{"anyOf":[{"type":"object","required":["token_url","auth_url","info_url","client_id","client_secret","scopes","redirect_url","user","display_name"],"properties":{"token_url":{"type":"string"},"auth_url":{"type":"string"},"info_url":{"type":"string"},"client_id":{"type":"string"},"client_secret":{"anyOf":[{"type":"object","required":["env"],"properties":{"env":{"type":"string","description":"Secret is stored in the env var"}}},{"type":"object","required":["inline"],"properties":{"inline":{"type":"string","description":"Secret is inline here"}}}]},"scopes":{"type":"array","items":{"type":"string"}},"redirect_url":{"type":"string"},"user":{"type":"object","required":["unique_id","name"],"properties":{"unique_id":{"type":"string","description":"A unique identifier for this provider","default":"email"},"name":{"type":"string","description":"A name for this provider","default":"name"}},"default":{"unique_id":"email","name":"name"}},"display_name":{"type":"string"},"color":{"type":"object","properties":{"default":{"type":"string"},"hover":{"type":"string"}}}}},{"type":"object","required":["openid_url","client_id","client_secret","scopes","redirect_url","user","display_name"],"properties":{"openid_url":{"type":"string"},"client_id":{"type":"string"},"client_secret":{"anyOf":[{"type":"object","required":["env"],"properties":{"env":{"type":"string","description":"Secret is stored in the env var"}}},{"type":"object","required":["inline"],"properties":{"inline":{"type":"string","description":"Secret is inline here"}}}]},"scopes":{"type":"array","items":{"type":"string"}},"redirect_url":{"type":"string"},"user":{"type":"object","required":["unique_id","name"],"properties":{"unique_id":{"type":"string","description":"A unique identifier for this provider","default":"email"},"name":{"type":"string","description":"A name for this provider","default":"name"}},"default":{"unique_id":"email","name":"name"}},"display_name":{"type":"string"},"color":{"type":"object","properties":{"default":{"type":"string"},"hover":{"type":"string"}}}}}]}}},"$schema":{"type":"string"}}}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ scopes = ["any needed scope here", "openid", "email"]
|
|||
redirect_url = "https://local.maix.me:8888/api/auth/oauth2/provider-openid/callback"
|
||||
# from the `info_url` request, which json key we will take an unique provider id (default:email) and an name for the user (default:name)
|
||||
user = { unique_id = "email", name = "name" }
|
||||
display_name = "OpenID 1"
|
||||
|
||||
[providers.discord]
|
||||
auth_url = "https://discord.com/oauth2/authorize"
|
||||
|
|
@ -19,3 +20,4 @@ client_id = "CLIENT_ID"
|
|||
redirect_url = "https://local.maix.me:8888/api/auth/oauth2/discord/callback"
|
||||
scopes = ["identify"] # here no email asked :)
|
||||
user = { unique_id = "id", name = "username" } # for example discord provides some stuff, like unique_id and username, such that we dont have to ask additional permission to get the email
|
||||
display_name = "Discord"
|
||||
|
|
|
|||
|
|
@ -195,6 +195,86 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/auth/providerList": {
|
||||
"get": {
|
||||
"operationId": "providerList",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg",
|
||||
"payload"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"success"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"providerList.success"
|
||||
]
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"list"
|
||||
],
|
||||
"properties": {
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"display_name",
|
||||
"name",
|
||||
"colors"
|
||||
],
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string",
|
||||
"description": "Name to display to the user"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "internal Name of the provider"
|
||||
},
|
||||
"colors": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"normal",
|
||||
"hover"
|
||||
],
|
||||
"properties": {
|
||||
"normal": {
|
||||
"type": "string",
|
||||
"description": "Default color for the provider"
|
||||
},
|
||||
"hover": {
|
||||
"type": "string",
|
||||
"description": "Hover color for the provider"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/auth/guest": {
|
||||
"post": {
|
||||
"operationId": "guestLogin",
|
||||
|
|
|
|||
|
|
@ -1,19 +1,11 @@
|
|||
import { isNullish } from '@shared/utils';
|
||||
import fp from 'fastify-plugin';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { access, constants as fsConstants, readFile } from 'node:fs/promises';
|
||||
import * as T from 'typebox';
|
||||
import * as V from 'typebox/value';
|
||||
import { Oauth2 } from '../oauth2';
|
||||
import { parseTOML } from 'confbox';
|
||||
|
||||
/*
|
||||
function isNullish<T>(_v: T): boolean { return true; }
|
||||
class Oauth2 {
|
||||
constructor(..._args: any[]) { }
|
||||
static fromProvider(..._args: any[]): Oauth2 { throw 'yes'; }
|
||||
}
|
||||
*/
|
||||
|
||||
const ProviderSecret = T.Union([
|
||||
T.Object({
|
||||
env: T.String({ description: 'Secret is stored in the env var' }),
|
||||
|
|
@ -21,10 +13,19 @@ const ProviderSecret = T.Union([
|
|||
T.Object({ inline: T.String({ description: 'Secret is inline here' }) }),
|
||||
]);
|
||||
|
||||
const ProviderUserInfo = T.Object({
|
||||
unique_id: T.String({ description: 'A unique identifier for this provider', default: 'email' }),
|
||||
name: T.String({ description: 'A name for this provider', default: 'name' }),
|
||||
}, { default: { unique_id: 'email', name: 'name' } });
|
||||
const ProviderUserInfo = T.Object(
|
||||
{
|
||||
unique_id: T.String({
|
||||
description: 'A unique identifier for this provider',
|
||||
default: 'email',
|
||||
}),
|
||||
name: T.String({
|
||||
description: 'A name for this provider',
|
||||
default: 'name',
|
||||
}),
|
||||
},
|
||||
{ default: { unique_id: 'email', name: 'name' } },
|
||||
);
|
||||
|
||||
const RawProviderBase = {
|
||||
client_id: T.String(),
|
||||
|
|
@ -32,6 +33,13 @@ const RawProviderBase = {
|
|||
scopes: T.Array(T.String()),
|
||||
redirect_url: T.String(),
|
||||
user: ProviderUserInfo,
|
||||
display_name: T.String(),
|
||||
color: T.Optional(
|
||||
T.Object({
|
||||
default: T.Optional(T.String()),
|
||||
hover: T.Optional(T.String()),
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
const ProviderBase = T.Object(RawProviderBase);
|
||||
|
|
@ -49,6 +57,8 @@ const ProviderMapFile = T.Object({
|
|||
$schema: T.Optional(T.String()),
|
||||
});
|
||||
|
||||
// console.log(JSON.stringify(ProviderMapFile))
|
||||
|
||||
export type ProviderSecret = T.Static<typeof ProviderSecret>;
|
||||
export type ProviderUserInfo = T.Static<typeof ProviderUserInfo>;
|
||||
export type ProviderBase = T.Static<typeof ProviderBase>;
|
||||
|
|
@ -58,10 +68,15 @@ export type Provider = T.Static<typeof Provider>;
|
|||
export type ProviderMap = T.Static<typeof ProviderMap>;
|
||||
|
||||
export type ProviderMapFile = T.Static<typeof ProviderMapFile>;
|
||||
|
||||
async function buildProviderMap(): Promise<ProviderMap> {
|
||||
const providerFile = process.env.PROVIDER_FILE;
|
||||
if (isNullish(providerFile)) throw 'PROVIDER_FILE env var not provided';
|
||||
if (isNullish(providerFile)) return {};
|
||||
try {
|
||||
await access(providerFile, fsConstants.F_OK | fsConstants.R_OK);
|
||||
}
|
||||
catch {
|
||||
return {};
|
||||
}
|
||||
const data = await readFile(providerFile, { encoding: 'utf-8' });
|
||||
const dataJson = parseTOML(data);
|
||||
return V.Parse(ProviderMapFile, dataJson).providers;
|
||||
|
|
@ -73,7 +88,9 @@ declare module 'fastify' {
|
|||
oauth2: { [k: string]: Oauth2 };
|
||||
}
|
||||
}
|
||||
async function makeAllOauth2(providers: ProviderMap): Promise<{ [k: string]: Oauth2 }> {
|
||||
async function makeAllOauth2(
|
||||
providers: ProviderMap,
|
||||
): Promise<{ [k: string]: Oauth2 }> {
|
||||
const out: { [k: string]: Oauth2 } = {};
|
||||
for (const [k, v] of Object.entries(providers)) {
|
||||
out[k] = await Oauth2.fromProvider(k, v);
|
||||
|
|
|
|||
46
src/auth/src/routes/getProviderList.ts
Normal file
46
src/auth/src/routes/getProviderList.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { FastifyPluginAsync } from 'fastify';
|
||||
|
||||
import { Type } from 'typebox';
|
||||
import { typeResponse, MakeStaticResponse } from '@shared/utils';
|
||||
|
||||
export const ProviderListRes = {
|
||||
'200': typeResponse('success', 'providerList.success', {
|
||||
list: Type.Array(Type.Object({
|
||||
display_name: Type.String({ description: 'Name to display to the user' }),
|
||||
name: Type.String({ description: 'internal Name of the provider' }),
|
||||
colors: Type.Object({
|
||||
normal: Type.String({ description: 'Default color for the provider' }),
|
||||
hover: Type.String({ description: 'Hover color for the provider' }),
|
||||
}),
|
||||
})),
|
||||
}),
|
||||
};
|
||||
|
||||
export type ProviderListRes = MakeStaticResponse<typeof ProviderListRes>;
|
||||
|
||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||
void _opts;
|
||||
fastify.get<{ Reply: ProviderListRes }>(
|
||||
'/api/auth/providerList',
|
||||
{ schema: { response: ProviderListRes, operationId: 'providerList' } },
|
||||
async function(req, res) {
|
||||
void req;
|
||||
|
||||
const list = Object.entries(this.providers).map(([providerName, provider]) => {
|
||||
const colors = provider.color ?? {};
|
||||
return {
|
||||
display_name: provider.display_name,
|
||||
name: providerName,
|
||||
colors: {
|
||||
normal: colors.default ?? 'bg-blue-600',
|
||||
hover: colors.hover ?? 'bg-blue-700',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return res.makeResponse(200, 'success', 'providerList.success', { list });
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export default route;
|
||||
|
|
@ -18,7 +18,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
|||
|
||||
const [url, _csrf, _nonce] = u.intoUrl();
|
||||
void _csrf; void _nonce;
|
||||
return res.setCookie('pkce', verifier.secret).redirect(url.toString());
|
||||
return res.setCookie('pkce', verifier.secret, { path:'/' }).redirect(url.toString());
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue