mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-21 14:17:11 +00:00
feat: add PKCE
This commit is contained in:
parent
f9b1096ded
commit
c4bb7f61f8
|
@ -50,6 +50,7 @@ interface ProviderTextFieldProps extends ProviderFieldProps {
|
|||
}
|
||||
|
||||
type ProviderBooleanProperties =
|
||||
| "use_pkce"
|
||||
| "enabled"
|
||||
| "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 ?? false}
|
||||
onInput={linkEvent(
|
||||
{
|
||||
modal: this,
|
||||
property: "use_pkce",
|
||||
},
|
||||
handleBooleanPropertyChange,
|
||||
)}
|
||||
/>
|
||||
<ProviderCheckboxField
|
||||
id="oauth-enabled"
|
||||
i18nKey="oauth_enabled"
|
||||
|
|
|
@ -25,6 +25,7 @@ import { UnreadCounterService } from "../../services";
|
|||
import { RouteData } from "../../interfaces";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||
import { generatePKCE } from "@utils/helpers/oauth";
|
||||
|
||||
interface LoginProps {
|
||||
prev?: string;
|
||||
|
@ -126,22 +127,31 @@ export async function handleUseOAuthProvider(params: {
|
|||
const redirectUri = `${window.location.origin}/oauth/callback`;
|
||||
|
||||
const state = crypto.randomUUID();
|
||||
const [code_challenge, code_verifier] = await generatePKCE();
|
||||
|
||||
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}`,
|
||||
...(params.oauth_provider.use_pkce
|
||||
? [
|
||||
`code_challenge=${encodeURIComponent(code_challenge)}`,
|
||||
"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(
|
||||
"oauth_state",
|
||||
JSON.stringify({
|
||||
state,
|
||||
pkce_code_verifier: code_verifier,
|
||||
oauth_provider_id: params.oauth_provider.id,
|
||||
redirect_uri: redirectUri,
|
||||
prev: params.prev ?? "/",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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
|
||||
|
|
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