Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,29 @@
# Can be generated with e.g. pwgen -s 64 1
# Please provide a string of length 64+ characters
# SESSION_SECRET=

# Enable login with OAuth 2.0
# OAUTH2_ENABLED: false
# OAUTH2_CLIENT_ID: ctfnote
# OAUTH2_CLIENT_SECRET: insecure_secret
# OAUTH2_SCOPE: openid profile groups

# The attribute to use as login and username
# OAUTH2_USERNAME_ATTR: name

# The attribute to use for determining the user's role
# The attribute can either be a string or an array of strings
# In case of an array, the highest role will be selected
# OAUTH2_ROLE_ATTR: groups

# A mapping for the values of the attribute to roles in CTFNote
# roles: user_admin, user_manager, user_member, user_fried, user_guest, none (no access to CTFNote)
# OAUTH2_ROLE_MAPPING: '{"admin": "user_admin"}'

# Either specify the discovery url or all other properties
# If a discovery url is provided, the other properties overwrite the values from the discovery
# OAUTH2_DISCOVERY_URL: https://example.com/.well-known/openid-configuration
# OAUTH2_ISSUER: https://example.com
# OAUTH2_AUTHORIZATION_ENDPOINT: https://example.com/api/oidc/authorization
# OAUTH2_TOKEN_ENDPOINT: https://example.com/api/oidc/token
# OAUTH2_USERINFO_ENDPOINT: https://example.com/api/oidc/userinfo
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

################################################################################
# Build stage 1 - `yarn build`
Expand Down
25 changes: 25 additions & 0 deletions api/migrations/57-external-login.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- Login with external identity and register/migrate user if not present/externally managed
CREATE FUNCTION ctfnote_private.login_with_extern("name" text, "role" ctfnote.role)
RETURNS ctfnote.jwt
AS $$
DECLARE
log_user ctfnote_private.user;
BEGIN
INSERT INTO ctfnote_private.user ("login", "password", "role")
VALUES (login_with_extern.name, 'external', login_with_extern.role)
ON CONFLICT ("login") DO UPDATE
SET password = 'external', role = login_with_extern.role
RETURNING
* INTO log_user;
INSERT INTO ctfnote.profile ("id", "username")
VALUES (log_user.id, login_with_extern.name)
ON CONFLICT (id) DO UPDATE
SET username = login_with_extern.name;
RETURN (ctfnote_private.new_token (log_user.id))::ctfnote.jwt;
END;
$$
LANGUAGE plpgsql
STRICT
SECURITY DEFINER;

GRANT EXECUTE ON FUNCTION ctfnote_private.login_with_extern TO user_anonymous;
5 changes: 5 additions & 0 deletions api/migrations/58-oauth2-login.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE ctfnote.settings
ADD COLUMN "oauth2_enabled" boolean NOT NULL DEFAULT FALSE;

GRANT SELECT ("oauth2_enabled") ON ctfnote.settings TO user_anonymous;
GRANT UPDATE ("oauth2_enabled") ON ctfnote.settings TO user_postgraphile;
1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"graphql": "^16.9.0",
"graphql-upload-ts": "^2.1.2",
"ical-generator": "^7.0.0",
"openid-client": "6.8.1",
"postgraphile": "4.13.0",
"postgraphile-plugin-connection-filter": "^2.3.0",
"postgres-migrations": "^5.3.0",
Expand Down
33 changes: 33 additions & 0 deletions api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ export type CTFNoteConfig = DeepReadOnly<{
registrationRoleId: string;
channelHandleStyle: DiscordChannelHandleStyle;
};
oauth2: {
enabled: string;
clientId: string;
clientSecret: string;
scope: string;
usernameAttr: string;
roleAttr: string;
roleMapping: string;
discoveryUrl: string;
authorizationEndpoint: string;
tokenEndpoint: string;
userinfoEndpoint: string;
tokenEndpointAuthMethod: string;
issuer: string;
};
}>;

function getEnv(
Expand Down Expand Up @@ -112,6 +127,24 @@ const config: CTFNoteConfig = {
"agile"
) as DiscordChannelHandleStyle,
},
oauth2: {
enabled: getEnv("OAUTH2_ENABLED", "false"),
clientId: getEnv("OAUTH2_CLIENT_ID", ""),
clientSecret: getEnv("OAUTH2_CLIENT_SECRET", ""),
scope: getEnv("OAUTH2_SCOPE", ""),
usernameAttr: getEnv("OAUTH2_USERNAME_ATTR", ""),
roleAttr: getEnv("OAUTH2_ROLE_ATTR", ""),
roleMapping: getEnv("OAUTH2_ROLE_MAPPING", ""),
discoveryUrl: getEnv("OAUTH2_DISCOVERY_URL", ""),
authorizationEndpoint: getEnv("OAUTH2_AUTHORIZATION_ENDPOINT", ""),
tokenEndpoint: getEnv("OAUTH2_TOKEN_ENDPOINT", ""),
userinfoEndpoint: getEnv("OAUTH2_USERINFO_ENDPOINT", ""),
tokenEndpointAuthMethod: getEnv(
"OAUTH2_TOKEN_ENDPOINT_AUTH_METHOD",
"client_secret_basic"
),
issuer: getEnv("OAUTH2_ISSUER", ""),
},
};

export default config;
2 changes: 1 addition & 1 deletion api/src/discord/agile/commands/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
} from "discord.js";
import { Command } from "../../interfaces/command";
import {
AllowedRoles,
createInvitationTokenForDiscordId,
getInvitationTokenForDiscordId,
getUserByDiscordId,
} from "../../database/users";
import { AllowedRoles } from "../../../utils/role";
import config from "../../../config";

async function getInvitationUrl(invitationCode: string | null = null) {
Expand Down
10 changes: 1 addition & 9 deletions api/src/discord/database/users.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { connectToDatabase } from "../../utils/database";
import { PoolClient } from "pg";
import { AllowedRoles } from "../../utils/role";

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

// refactor above to an enum
export enum AllowedRoles {
user_guest = "user_guest",
user_friend = "user_friend",
user_member = "user_member",
user_manager = "user_manager",
user_admin = "user_admin",
}

export async function getInvitationTokenForDiscordId(
discordId: string,
pgClient: PoolClient | null = null
Expand Down
13 changes: 11 additions & 2 deletions api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@ import uploadLogoPlugin from "./plugins/uploadLogo";
import uploadScalar from "./plugins/uploadScalar";
import { Pool } from "pg";
import { icalRoute } from "./routes/ical";
import { oauth2Router } from "./routes/oauth2";
import ConnectionFilterPlugin from "postgraphile-plugin-connection-filter";
import OperationHook from "@graphile/operation-hooks";
import discordHooks from "./discord/hooks";
import { initDiscordBot } from "./discord";
import PgManyToManyPlugin from "@graphile-contrib/pg-many-to-many";
import ProfileSubscriptionPlugin from "./plugins/ProfileSubscriptionPlugin";
import {
checkOAuth2Enabled,
loginWithOAuth2Plugin,
} from "./plugins/loginWithOAuth2";

function getDbUrl(role: "user" | "admin") {
const login = config.db[role].login;
Expand Down Expand Up @@ -63,6 +68,7 @@ function createOptions() {
discordHooks,
PgManyToManyPlugin,
ProfileSubscriptionPlugin,
loginWithOAuth2Plugin,
],
ownerConnectionString: getDbUrl("admin"),
enableQueryBatching: true,
Expand Down Expand Up @@ -109,7 +115,7 @@ function createOptions() {
return postgraphileOptions;
}

function createApp(postgraphileOptions: PostGraphileOptions) {
async function createApp(postgraphileOptions: PostGraphileOptions) {
const pool = new Pool({
connectionString: getDbUrl("user"),
});
Expand All @@ -126,6 +132,9 @@ function createApp(postgraphileOptions: PostGraphileOptions) {
);
app.use(postgraphile(pool, "ctfnote", postgraphileOptions));
app.use("/calendar.ics", icalRoute(pool));
if (await checkOAuth2Enabled()) {
app.use("/api/auth/oauth2", oauth2Router);
}
return app;
}

Expand All @@ -150,7 +159,7 @@ async function main() {
return;
}
const postgraphileOptions = createOptions();
const app = createApp(postgraphileOptions);
const app = await createApp(postgraphileOptions);

await initDiscordBot();

Expand Down
Loading
Loading