feat: add PKCE

This commit is contained in:
avdb13 2024-11-24 00:06:54 +00:00
parent c75fe0b0dc
commit bb3ab9b903
7 changed files with 71 additions and 10 deletions

View file

@ -60,7 +60,7 @@
"inferno-router": "^8.2.3",
"inferno-server": "^8.2.3",
"jwt-decode": "^4.0.0",
"lemmy-js-client": "0.20.0-alpha.17",
"lemmy-js-client": "0.20.0-pkce.1",
"lodash.isequal": "^4.5.0",
"markdown-it": "^14.1.0",
"markdown-it-bidi": "^0.2.0",

View file

@ -51,6 +51,7 @@ interface ProviderTextFieldProps extends ProviderFieldProps {
type ProviderBooleanProperties =
| "enabled"
| "use_pkce"
| "account_linking_enabled"
| "auto_verify_email";
@ -337,6 +338,18 @@ export default class CreateOrEditOAuthProviderModal extends Component<
handleBooleanPropertyChange,
)}
/>
<ProviderCheckboxField
id="use-pkce"
i18nKey="use_pkce"
checked={provider?.use_pkce}
onInput={linkEvent(
{
modal: this,
property: "use_pkce",
},
handleBooleanPropertyChange,
)}
/>
<ProviderCheckboxField
id="oauth-enabled"
i18nKey="oauth_enabled"

View file

@ -25,6 +25,10 @@ import { UnreadCounterService } from "../../services";
import { RouteData } from "../../interfaces";
import { IRoutePropsWithFetch } from "../../routes";
import { simpleScrollMixin } from "../mixins/scroll-mixin";
import {
generateCodeVerifier,
createCodeChallenge,
} from "@utils/helpers/oauth";
interface LoginProps {
prev?: string;
@ -126,16 +130,32 @@ export async function handleUseOAuthProvider(params: {
const redirectUri = `${window.location.origin}/oauth/callback`;
const state = crypto.randomUUID();
let codeVerifier: string | undefined;
if (params.oauth_provider.use_pkce) {
codeVerifier = generateCodeVerifier();
}
let codeChallenge: string | undefined;
if (codeVerifier) {
codeChallenge = await createCodeChallenge(codeVerifier);
}
const queryPairs = [
`client_id=${encodeURIComponent(params.oauth_provider.client_id)}`,
`response_type=code`,
`scope=${encodeURIComponent(params.oauth_provider.scopes)}`,
`redirect_uri=${encodeURIComponent(redirectUri)}`,
`state=${state}`,
...(codeChallenge
? [
`code_challenge=${encodeURIComponent(codeChallenge)}`,
"code_challenge_method=S256",
]
: []),
];
const requestUri =
params.oauth_provider.authorization_endpoint +
"?" +
[
`client_id=${encodeURIComponent(params.oauth_provider.client_id)}`,
`response_type=code`,
`scope=${encodeURIComponent(params.oauth_provider.scopes)}`,
`redirect_uri=${encodeURIComponent(redirectUri)}`,
`state=${state}`,
].join("&");
params.oauth_provider.authorization_endpoint + "?" + queryPairs.join("&");
// store state in local storage
localStorage.setItem(
@ -149,6 +169,7 @@ export async function handleUseOAuthProvider(params: {
answer: params.answer,
show_nsfw: params.show_nsfw,
expires_at: Date.now() + 5 * 60_000,
...(codeVerifier ? { pkce_code_verifier: codeVerifier } : {}),
}),
);

View file

@ -79,6 +79,9 @@ export class OAuthCallback extends Component<OAuthCallbackRouteProps, State> {
show_nsfw: local_oauth_state.show_nsfw,
username: local_oauth_state.username,
answer: local_oauth_state.answer,
...(local_oauth_state?.pkce_code_verifier && {
pkce_code_verifier: local_oauth_state.pkce_code_verifier,
}),
});
switch (loginRes.state) {

View file

@ -86,6 +86,10 @@ export default function OAuthProviderListItem({
i18nKey="oauth_account_linking_enabled"
data={boolToYesNo(provider.account_linking_enabled)}
/>
<TextInfoField
i18nKey="use_pkce"
data={boolToYesNo(provider.use_pkce)}
/>
<TextInfoField
i18nKey="oauth_enabled"
data={boolToYesNo(provider.enabled)}

View file

@ -36,6 +36,7 @@ const PRESET_OAUTH_PROVIDERS: ProviderToEdit[] = [
scopes: "openid email",
auto_verify_email: true,
account_linking_enabled: true,
use_pkce: true,
enabled: true,
},
// additional preset providers can be added here

View file

@ -0,0 +1,19 @@
export function base64URLEncode(buffer: Uint8Array | ArrayBuffer) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)))
.replace(/\//g, "_")
.replace(/\+/g, "-")
.replace(/=/g, "");
}
export function generateCodeVerifier(length: number = 64) {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
return base64URLEncode(array);
}
export async function createCodeChallenge(codeVerifier: string) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await window.crypto.subtle.digest("SHA-256", data);
return base64URLEncode(digest);
}