Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | 43x 43x 19x 19x 43x 28x 28x 54x 37x 28x 43x 23x 23x 43x | import { AuthEndpoint } from "shared/constants/apiEndpoints";
import {
USERNAME_LENGTH_MAX,
USERNAME_LENGTH_MIN,
} from "shared/constants/validation";
import { KompassiUserinfo } from "server/features/kompassi-login/KompassiLoginTypes";
import {
clientId,
getBaseUrl,
} from "server/features/kompassi-login/kompassiLoginService";
// openid is required for the userinfo endpoint to accept the access token at
// all; profile and email are conventional for the claims that come with it
const scope = "openid profile email";
export const getAuthUrl = (origin: string, state: string): string => {
const params = new URLSearchParams({
response_type: "code",
client_id: clientId,
redirect_uri: `${origin}${AuthEndpoint.KOMPASSI_LOGIN_CALLBACK}`,
scope,
state,
});
return `${getBaseUrl()}/oidc/authorize/?${params.toString()}`;
};
// OIDC carries no username claim, so a new account needs a starting name for
// the form where the user confirms it. Kompassi's name claim is the full name,
// which usually quotes the nick - that's the closest thing to a chosen handle.
// Candidates that the form itself would reject are skipped rather than
// prefilled, and the Kompassi id backstop is never empty: an empty username
// still saves (the `required` validator accepts empty strings) into an account
// whose JWT then fails every authenticated request
export const deriveKonstiUsername = (userinfo: KompassiUserinfo): string => {
const candidates = [
// Kompassi quotes the nick with ASCII quotes today, but a name typed in an
// editor that curls them would otherwise fall through to the real name
/["“”]([^"“”]+)["“”]/.exec(userinfo.name)?.[1],
userinfo.given_name,
userinfo.email.split("@", 1)[0],
];
const usable = candidates
.map((candidate) => candidate?.trim() ?? "")
.find((candidate) => candidate.length >= USERNAME_LENGTH_MIN);
return (usable ?? `kompassi-${userinfo.sub}`).slice(0, USERNAME_LENGTH_MAX);
};
// Trims the name rather than the suffix, so the result stays unique to this
// Kompassi account even when the name uses up the whole length budget. The
// Kompassi id is opaque and could be long enough to crowd the name out
// entirely, so it too is capped - leaving a name worth reading, and a total
// the finalize form will accept
export const addKompassiIdSuffix = (username: string, sub: string): string => {
const suffix = `-${sub.slice(0, USERNAME_LENGTH_MAX - USERNAME_LENGTH_MIN - 1)}`;
return `${username.slice(0, USERNAME_LENGTH_MAX - suffix.length)}${suffix}`;
};
// The response body is logged when validation fails - hide token values in
// case the failure is a partially valid body rather than an OAuth error object
export const redactTokenValues = (data: unknown): unknown => {
if (typeof data !== "object" || data === null) {
return data;
}
const secretKeys = new Set(["access_token", "refresh_token", "id_token"]);
return Object.fromEntries(
Object.entries(data).map(([key, value]) => [
key,
secretKeys.has(key) ? "[redacted]" : value,
]),
);
};
|