Skip to content

Commit b76ff1d

Browse files
committed
Add OAuth2 login
1 parent a2a3985 commit b76ff1d

29 files changed

+599
-13
lines changed
95 KB
Binary file not shown.
55.9 KB
Binary file not shown.
Binary file not shown.

api/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Global args, set before the first FROM, shared by all stages
44
ARG NODE_ENV="production"
5-
ARG NODE_IMAGE="18.19.1-alpine"
5+
ARG NODE_IMAGE="20.19.5-alpine"
66

77
################################################################################
88
# Build stage 1 - `yarn build`
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
-- Login with external identity and register/migrate user if not present/externally managed
2+
CREATE FUNCTION ctfnote_private.login_with_extern("name" text, "role" ctfnote.role)
3+
RETURNS ctfnote.jwt
4+
AS $$
5+
DECLARE
6+
log_user ctfnote_private.user;
7+
BEGIN
8+
INSERT INTO ctfnote_private.user ("login", "password", "role")
9+
VALUES (login_with_extern.name, 'external', login_with_extern.role)
10+
ON CONFLICT ("login") DO UPDATE
11+
SET login = login_with_extern.name, password = 'external'
12+
RETURNING
13+
* INTO log_user;
14+
INSERT INTO ctfnote.profile ("id", "username")
15+
VALUES (log_user.id, login_with_extern.name)
16+
ON CONFLICT (id) DO UPDATE
17+
SET username = login_with_extern.name;
18+
RETURN (ctfnote_private.new_token (log_user.id))::ctfnote.jwt;
19+
END;
20+
$$
21+
LANGUAGE plpgsql
22+
STRICT
23+
SECURITY DEFINER;
24+
25+
GRANT EXECUTE ON FUNCTION ctfnote_private.login_with_extern TO user_anonymous;

api/migrations/58-oauth2-login.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ALTER TABLE ctfnote.settings
2+
ADD COLUMN "oauth2_enabled" boolean NOT NULL DEFAULT FALSE;
3+
4+
GRANT SELECT ("oauth2_enabled") ON ctfnote.settings TO user_anonymous;
5+
GRANT UPDATE ("oauth2_enabled") ON ctfnote.settings TO user_postgraphile;

api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"graphql": "^16.9.0",
3131
"graphql-upload-ts": "^2.1.2",
3232
"ical-generator": "^7.0.0",
33+
"openid-client": "6.8.1",
3334
"postgraphile": "4.13.0",
3435
"postgraphile-plugin-connection-filter": "^2.3.0",
3536
"postgres-migrations": "^5.3.0",

api/src/config.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@ export type CTFNoteConfig = DeepReadOnly<{
5151
registrationRoleId: string;
5252
channelHandleStyle: DiscordChannelHandleStyle;
5353
};
54+
oauth2: {
55+
enabled: string;
56+
clientId: string;
57+
clientSecret: string;
58+
scope: string;
59+
usernameAttr: string;
60+
roleAttr: string;
61+
roleMapping: string;
62+
discoveryUrl: string;
63+
authorizationEndpoint: string;
64+
tokenEndpoint: string;
65+
userinfoEndpoint: string;
66+
tokenEndpointAuthMethod: string;
67+
issuer: string;
68+
};
5469
}>;
5570

5671
function getEnv(
@@ -112,6 +127,24 @@ const config: CTFNoteConfig = {
112127
"agile"
113128
) as DiscordChannelHandleStyle,
114129
},
130+
oauth2: {
131+
enabled: getEnv("OAUTH2_ENABLED", "false"),
132+
clientId: getEnv("OAUTH2_CLIENT_ID", ""),
133+
clientSecret: getEnv("OAUTH2_CLIENT_SECRET", ""),
134+
scope: getEnv("OAUTH2_SCOPE", "openid profile roles"),
135+
usernameAttr: getEnv("OAUTH2_USERNAME_ATTR", ""),
136+
roleAttr: getEnv("OAUTH2_ROLE_ATTR", ""),
137+
roleMapping: getEnv("OAUTH2_ROLE_MAPPING", ""),
138+
discoveryUrl: getEnv("OAUTH2_DISCOVERY_URL", ""),
139+
authorizationEndpoint: getEnv("OAUTH2_AUTHORIZATION_ENDPOINT", ""),
140+
tokenEndpoint: getEnv("OAUTH2_TOKEN_ENDPOINT", ""),
141+
userinfoEndpoint: getEnv("OAUTH2_USERINFO_ENDPOINT", ""),
142+
tokenEndpointAuthMethod: getEnv(
143+
"OAUTH2_TOKEN_ENDPOINT_AUTH_METHOD",
144+
"client_secret_basic"
145+
),
146+
issuer: getEnv("OAUTH2_ISSUER", ""),
147+
},
115148
};
116149

117150
export default config;

api/src/discord/agile/commands/register.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import {
66
} from "discord.js";
77
import { Command } from "../../interfaces/command";
88
import {
9-
AllowedRoles,
109
createInvitationTokenForDiscordId,
1110
getInvitationTokenForDiscordId,
1211
getUserByDiscordId,
1312
} from "../../database/users";
13+
import { AllowedRoles } from "../../../utils/role";
1414
import config from "../../../config";
1515

1616
async function getInvitationUrl(invitationCode: string | null = null) {

api/src/discord/database/users.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { connectToDatabase } from "../../utils/database";
22
import { PoolClient } from "pg";
3+
import { AllowedRoles } from "../../utils/role";
34

45
/*
56
* Only returns users that have not linked their discord account yet.
@@ -45,15 +46,6 @@ export async function setDiscordIdForUser(
4546
}
4647
}
4748

48-
// refactor above to an enum
49-
export enum AllowedRoles {
50-
user_guest = "user_guest",
51-
user_friend = "user_friend",
52-
user_member = "user_member",
53-
user_manager = "user_manager",
54-
user_admin = "user_admin",
55-
}
56-
5749
export async function getInvitationTokenForDiscordId(
5850
discordId: string,
5951
pgClient: PoolClient | null = null

0 commit comments

Comments
 (0)