mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-29 15:51:14 +00:00
feat: add PKCE
This commit is contained in:
parent
f9b1096ded
commit
c4bb7f61f8
6 changed files with 84 additions and 9 deletions
|
@ -50,6 +50,7 @@ interface ProviderTextFieldProps extends ProviderFieldProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderBooleanProperties =
|
type ProviderBooleanProperties =
|
||||||
|
| "use_pkce"
|
||||||
| "enabled"
|
| "enabled"
|
||||||
| "account_linking_enabled"
|
| "account_linking_enabled"
|
||||||
| "auto_verify_email";
|
| "auto_verify_email";
|
||||||
|
@ -337,6 +338,18 @@ export default class CreateOrEditOAuthProviderModal extends Component<
|
||||||
handleBooleanPropertyChange,
|
handleBooleanPropertyChange,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<ProviderCheckboxField
|
||||||
|
id="use-pkce"
|
||||||
|
i18nKey="use_pkce"
|
||||||
|
checked={provider?.use_pkce ?? false}
|
||||||
|
onInput={linkEvent(
|
||||||
|
{
|
||||||
|
modal: this,
|
||||||
|
property: "use_pkce",
|
||||||
|
},
|
||||||
|
handleBooleanPropertyChange,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<ProviderCheckboxField
|
<ProviderCheckboxField
|
||||||
id="oauth-enabled"
|
id="oauth-enabled"
|
||||||
i18nKey="oauth_enabled"
|
i18nKey="oauth_enabled"
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { UnreadCounterService } from "../../services";
|
||||||
import { RouteData } from "../../interfaces";
|
import { RouteData } from "../../interfaces";
|
||||||
import { IRoutePropsWithFetch } from "../../routes";
|
import { IRoutePropsWithFetch } from "../../routes";
|
||||||
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||||
|
import { generatePKCE } from "@utils/helpers/oauth";
|
||||||
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
prev?: string;
|
prev?: string;
|
||||||
|
@ -126,22 +127,31 @@ export async function handleUseOAuthProvider(params: {
|
||||||
const redirectUri = `${window.location.origin}/oauth/callback`;
|
const redirectUri = `${window.location.origin}/oauth/callback`;
|
||||||
|
|
||||||
const state = crypto.randomUUID();
|
const state = crypto.randomUUID();
|
||||||
const requestUri =
|
const [code_challenge, code_verifier] = await generatePKCE();
|
||||||
params.oauth_provider.authorization_endpoint +
|
|
||||||
"?" +
|
const queryPairs = [
|
||||||
[
|
|
||||||
`client_id=${encodeURIComponent(params.oauth_provider.client_id)}`,
|
`client_id=${encodeURIComponent(params.oauth_provider.client_id)}`,
|
||||||
`response_type=code`,
|
`response_type=code`,
|
||||||
`scope=${encodeURIComponent(params.oauth_provider.scopes)}`,
|
`scope=${encodeURIComponent(params.oauth_provider.scopes)}`,
|
||||||
`redirect_uri=${encodeURIComponent(redirectUri)}`,
|
`redirect_uri=${encodeURIComponent(redirectUri)}`,
|
||||||
`state=${state}`,
|
`state=${state}`,
|
||||||
].join("&");
|
...(params.oauth_provider.use_pkce
|
||||||
|
? [
|
||||||
|
`code_challenge=${encodeURIComponent(code_challenge)}`,
|
||||||
|
"code_challenge_method=S256",
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const requestUri =
|
||||||
|
params.oauth_provider.authorization_endpoint + "?" + queryPairs.join("&");
|
||||||
|
|
||||||
// store state in local storage
|
// store state in local storage
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"oauth_state",
|
"oauth_state",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
state,
|
state,
|
||||||
|
pkce_code_verifier: code_verifier,
|
||||||
oauth_provider_id: params.oauth_provider.id,
|
oauth_provider_id: params.oauth_provider.id,
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
prev: params.prev ?? "/",
|
prev: params.prev ?? "/",
|
||||||
|
|
|
@ -79,6 +79,9 @@ export class OAuthCallback extends Component<OAuthCallbackRouteProps, State> {
|
||||||
show_nsfw: local_oauth_state.show_nsfw,
|
show_nsfw: local_oauth_state.show_nsfw,
|
||||||
username: local_oauth_state.username,
|
username: local_oauth_state.username,
|
||||||
answer: local_oauth_state.answer,
|
answer: local_oauth_state.answer,
|
||||||
|
...(local_oauth_state?.pkce_code_verifier && {
|
||||||
|
pkce_code_verifier: local_oauth_state.pkce_code_verifier,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (loginRes.state) {
|
switch (loginRes.state) {
|
||||||
|
|
|
@ -86,6 +86,10 @@ export default function OAuthProviderListItem({
|
||||||
i18nKey="oauth_account_linking_enabled"
|
i18nKey="oauth_account_linking_enabled"
|
||||||
data={boolToYesNo(provider.account_linking_enabled)}
|
data={boolToYesNo(provider.account_linking_enabled)}
|
||||||
/>
|
/>
|
||||||
|
<TextInfoField
|
||||||
|
i18nKey="use_pkce"
|
||||||
|
data={boolToYesNo(provider.use_pkce)}
|
||||||
|
/>
|
||||||
<TextInfoField
|
<TextInfoField
|
||||||
i18nKey="oauth_enabled"
|
i18nKey="oauth_enabled"
|
||||||
data={boolToYesNo(provider.enabled)}
|
data={boolToYesNo(provider.enabled)}
|
||||||
|
|
|
@ -36,6 +36,7 @@ const PRESET_OAUTH_PROVIDERS: ProviderToEdit[] = [
|
||||||
scopes: "openid email",
|
scopes: "openid email",
|
||||||
auto_verify_email: true,
|
auto_verify_email: true,
|
||||||
account_linking_enabled: true,
|
account_linking_enabled: true,
|
||||||
|
use_pkce: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
// additional preset providers can be added here
|
// additional preset providers can be added here
|
||||||
|
|
44
src/shared/utils/helpers/oauth.ts
Normal file
44
src/shared/utils/helpers/oauth.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
const PKCE_VERIFIER_LENGTH = 96;
|
||||||
|
|
||||||
|
const PKCE_ALPHABET =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
||||||
|
|
||||||
|
const PKCE_ALGORITHM = "SHA-256";
|
||||||
|
|
||||||
|
function urlUnpaddedBase64Encode(value: string): string {
|
||||||
|
return btoa(
|
||||||
|
String.fromCharCode.apply(
|
||||||
|
null,
|
||||||
|
new Uint8Array(new TextEncoder().encode(value)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generatePKCE(): Promise<[string, string]> {
|
||||||
|
const randomValues = crypto.getRandomValues(
|
||||||
|
new Uint32Array(PKCE_VERIFIER_LENGTH),
|
||||||
|
);
|
||||||
|
|
||||||
|
const code_verifier = urlUnpaddedBase64Encode(
|
||||||
|
Array.from(randomValues)
|
||||||
|
.map(n => PKCE_ALPHABET[n % PKCE_ALPHABET.length])
|
||||||
|
.join(""),
|
||||||
|
);
|
||||||
|
const code_verifier_digest = await crypto.subtle.digest(
|
||||||
|
PKCE_ALGORITHM,
|
||||||
|
new TextEncoder().encode(code_verifier),
|
||||||
|
);
|
||||||
|
const code_verifier_hash = new Uint8Array(code_verifier_digest);
|
||||||
|
|
||||||
|
let code_challenge = "";
|
||||||
|
for (let i = 0; i < code_verifier_hash.byteLength; i++) {
|
||||||
|
code_challenge = code_challenge.concat(
|
||||||
|
String.fromCharCode(code_verifier_hash[i]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [urlUnpaddedBase64Encode(code_challenge), code_verifier];
|
||||||
|
}
|
Loading…
Reference in a new issue