mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-22 19:01:26 +00:00
feat: add PKCE
This commit is contained in:
parent
c75fe0b0dc
commit
bb3ab9b903
7 changed files with 71 additions and 10 deletions
|
@ -60,7 +60,7 @@
|
||||||
"inferno-router": "^8.2.3",
|
"inferno-router": "^8.2.3",
|
||||||
"inferno-server": "^8.2.3",
|
"inferno-server": "^8.2.3",
|
||||||
"jwt-decode": "^4.0.0",
|
"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",
|
"lodash.isequal": "^4.5.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"markdown-it-bidi": "^0.2.0",
|
"markdown-it-bidi": "^0.2.0",
|
||||||
|
|
|
@ -51,6 +51,7 @@ interface ProviderTextFieldProps extends ProviderFieldProps {
|
||||||
|
|
||||||
type ProviderBooleanProperties =
|
type ProviderBooleanProperties =
|
||||||
| "enabled"
|
| "enabled"
|
||||||
|
| "use_pkce"
|
||||||
| "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}
|
||||||
|
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,10 @@ 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 {
|
||||||
|
generateCodeVerifier,
|
||||||
|
createCodeChallenge,
|
||||||
|
} from "@utils/helpers/oauth";
|
||||||
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
prev?: string;
|
prev?: string;
|
||||||
|
@ -126,16 +130,32 @@ 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();
|
||||||
|
|
||||||
|
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 =
|
const requestUri =
|
||||||
params.oauth_provider.authorization_endpoint +
|
params.oauth_provider.authorization_endpoint + "?" + queryPairs.join("&");
|
||||||
"?" +
|
|
||||||
[
|
|
||||||
`client_id=${encodeURIComponent(params.oauth_provider.client_id)}`,
|
|
||||||
`response_type=code`,
|
|
||||||
`scope=${encodeURIComponent(params.oauth_provider.scopes)}`,
|
|
||||||
`redirect_uri=${encodeURIComponent(redirectUri)}`,
|
|
||||||
`state=${state}`,
|
|
||||||
].join("&");
|
|
||||||
|
|
||||||
// store state in local storage
|
// store state in local storage
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
|
@ -149,6 +169,7 @@ export async function handleUseOAuthProvider(params: {
|
||||||
answer: params.answer,
|
answer: params.answer,
|
||||||
show_nsfw: params.show_nsfw,
|
show_nsfw: params.show_nsfw,
|
||||||
expires_at: Date.now() + 5 * 60_000,
|
expires_at: Date.now() + 5 * 60_000,
|
||||||
|
...(codeVerifier ? { pkce_code_verifier: codeVerifier } : {}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
19
src/shared/utils/helpers/oauth.ts
Normal file
19
src/shared/utils/helpers/oauth.ts
Normal 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);
|
||||||
|
}
|
Loading…
Reference in a new issue