mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-16 23:31:19 +03:00
OAuth PKCE is in.
Reference PR: https://github.com/mastodon/mastodon/pull/31129
This commit is contained in:
parent
2e6074d794
commit
7c56b64e8a
5 changed files with 138 additions and 28 deletions
12
src/app.jsx
12
src/app.jsx
|
@ -324,6 +324,7 @@ function App() {
|
||||||
const clientID = store.sessionCookie.get('clientID');
|
const clientID = store.sessionCookie.get('clientID');
|
||||||
const clientSecret = store.sessionCookie.get('clientSecret');
|
const clientSecret = store.sessionCookie.get('clientSecret');
|
||||||
const vapidKey = store.sessionCookie.get('vapidKey');
|
const vapidKey = store.sessionCookie.get('vapidKey');
|
||||||
|
const verifier = store.sessionCookie.get('codeVerifier');
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
|
@ -332,8 +333,10 @@ function App() {
|
||||||
client_id: clientID,
|
client_id: clientID,
|
||||||
client_secret: clientSecret,
|
client_secret: clientSecret,
|
||||||
code,
|
code,
|
||||||
|
code_verifier: verifier || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
const client = initClient({ instance: instanceURL, accessToken });
|
const client = initClient({ instance: instanceURL, accessToken });
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
initPreferences(client),
|
initPreferences(client),
|
||||||
|
@ -341,9 +344,13 @@ function App() {
|
||||||
initAccount(client, instanceURL, accessToken, vapidKey),
|
initAccount(client, instanceURL, accessToken, vapidKey),
|
||||||
]);
|
]);
|
||||||
initStates();
|
initStates();
|
||||||
|
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
|
||||||
|
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
|
} else {
|
||||||
|
setUIState('error');
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
} else {
|
} else {
|
||||||
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
|
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
|
||||||
|
@ -387,6 +394,11 @@ function App() {
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
store.sessionCookie.del('clientID');
|
||||||
|
store.sessionCookie.del('clientSecret');
|
||||||
|
store.sessionCookie.del('codeVerifier');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
let location = useLocation();
|
let location = useLocation();
|
||||||
|
|
|
@ -1337,7 +1337,7 @@ msgid "Accounts…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/nav-menu.jsx:363
|
#: src/components/nav-menu.jsx:363
|
||||||
#: src/pages/login.jsx:142
|
#: src/pages/login.jsx:166
|
||||||
#: src/pages/status.jsx:792
|
#: src/pages/status.jsx:792
|
||||||
#: src/pages/welcome.jsx:64
|
#: src/pages/welcome.jsx:64
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
|
@ -1733,7 +1733,7 @@ msgstr ""
|
||||||
#: src/components/shortcuts-settings.jsx:75
|
#: src/components/shortcuts-settings.jsx:75
|
||||||
#: src/components/shortcuts-settings.jsx:84
|
#: src/components/shortcuts-settings.jsx:84
|
||||||
#: src/components/shortcuts-settings.jsx:122
|
#: src/components/shortcuts-settings.jsx:122
|
||||||
#: src/pages/login.jsx:146
|
#: src/pages/login.jsx:170
|
||||||
msgid "Instance"
|
msgid "Instance"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -2353,7 +2353,7 @@ msgstr "Login required."
|
||||||
|
|
||||||
#: src/compose.jsx:90
|
#: src/compose.jsx:90
|
||||||
#: src/pages/http-route.jsx:91
|
#: src/pages/http-route.jsx:91
|
||||||
#: src/pages/login.jsx:223
|
#: src/pages/login.jsx:247
|
||||||
msgid "Go home"
|
msgid "Go home"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -3025,23 +3025,28 @@ msgstr ""
|
||||||
msgid "No lists yet."
|
msgid "No lists yet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/login.jsx:185
|
#: src/pages/login.jsx:86
|
||||||
|
#: src/pages/login.jsx:99
|
||||||
|
msgid "Failed to register application"
|
||||||
|
msgstr "Failed to register application"
|
||||||
|
|
||||||
|
#: src/pages/login.jsx:209
|
||||||
msgid "e.g. “mastodon.social”"
|
msgid "e.g. “mastodon.social”"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/login.jsx:196
|
#: src/pages/login.jsx:220
|
||||||
msgid "Failed to log in. Please try again or try another instance."
|
msgid "Failed to log in. Please try again or try another instance."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/login.jsx:208
|
#: src/pages/login.jsx:232
|
||||||
msgid "Continue with {selectedInstanceText}"
|
msgid "Continue with {selectedInstanceText}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/login.jsx:209
|
#: src/pages/login.jsx:233
|
||||||
msgid "Continue"
|
msgid "Continue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/login.jsx:217
|
#: src/pages/login.jsx:241
|
||||||
msgid "Don't have an account? Create one!"
|
msgid "Don't have an account? Create one!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,12 @@ import LangSelector from '../components/lang-selector';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import Loader from '../components/loader';
|
import Loader from '../components/loader';
|
||||||
import instancesListURL from '../data/instances.json?url';
|
import instancesListURL from '../data/instances.json?url';
|
||||||
import { getAuthorizationURL, registerApplication } from '../utils/auth';
|
import {
|
||||||
|
getAuthorizationURL,
|
||||||
|
getPKCEAuthorizationURL,
|
||||||
|
registerApplication,
|
||||||
|
} from '../utils/auth';
|
||||||
|
import { supportsPKCE } from '../utils/oauth-pkce';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
@ -63,6 +68,24 @@ function Login() {
|
||||||
instanceURL,
|
instanceURL,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const authPKCE = await supportsPKCE({ instanceURL });
|
||||||
|
console.log({ authPKCE });
|
||||||
|
if (authPKCE) {
|
||||||
|
if (client_id && client_secret) {
|
||||||
|
store.sessionCookie.set('clientID', client_id);
|
||||||
|
store.sessionCookie.set('clientSecret', client_secret);
|
||||||
|
store.sessionCookie.set('vapidKey', vapid_key);
|
||||||
|
|
||||||
|
const [url, verifier] = await getPKCEAuthorizationURL({
|
||||||
|
instanceURL,
|
||||||
|
client_id,
|
||||||
|
});
|
||||||
|
store.sessionCookie.set('codeVerifier', verifier);
|
||||||
|
location.href = url;
|
||||||
|
} else {
|
||||||
|
alert(t`Failed to register application`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (client_id && client_secret) {
|
if (client_id && client_secret) {
|
||||||
store.sessionCookie.set('clientID', client_id);
|
store.sessionCookie.set('clientID', client_id);
|
||||||
store.sessionCookie.set('clientSecret', client_secret);
|
store.sessionCookie.set('clientSecret', client_secret);
|
||||||
|
@ -73,7 +96,8 @@ function Login() {
|
||||||
client_id,
|
client_id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
alert('Failed to register application');
|
alert(t`Failed to register application`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { generateCodeChallenge, verifier } from './oauth-pkce';
|
||||||
|
|
||||||
const { PHANPY_CLIENT_NAME: CLIENT_NAME, PHANPY_WEBSITE: WEBSITE } = import.meta
|
const { PHANPY_CLIENT_NAME: CLIENT_NAME, PHANPY_WEBSITE: WEBSITE } = import.meta
|
||||||
.env;
|
.env;
|
||||||
|
|
||||||
|
@ -25,6 +27,21 @@ export async function registerApplication({ instanceURL }) {
|
||||||
return registrationJSON;
|
return registrationJSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getPKCEAuthorizationURL({ instanceURL, client_id }) {
|
||||||
|
const codeVerifier = verifier();
|
||||||
|
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id,
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
code_challenge: codeChallenge,
|
||||||
|
redirect_uri: location.origin + location.pathname,
|
||||||
|
response_type: 'code',
|
||||||
|
scope: SCOPES,
|
||||||
|
});
|
||||||
|
const authorizationURL = `https://${instanceURL}/oauth/authorize?${params.toString()}`;
|
||||||
|
return [authorizationURL, codeVerifier];
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAuthorizationURL({ instanceURL, client_id }) {
|
export async function getAuthorizationURL({ instanceURL, client_id }) {
|
||||||
const authorizationParams = new URLSearchParams({
|
const authorizationParams = new URLSearchParams({
|
||||||
client_id,
|
client_id,
|
||||||
|
@ -42,15 +59,23 @@ export async function getAccessToken({
|
||||||
client_id,
|
client_id,
|
||||||
client_secret,
|
client_secret,
|
||||||
code,
|
code,
|
||||||
|
code_verifier,
|
||||||
}) {
|
}) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id,
|
client_id,
|
||||||
client_secret,
|
|
||||||
redirect_uri: location.origin + location.pathname,
|
redirect_uri: location.origin + location.pathname,
|
||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
code,
|
code,
|
||||||
scope: SCOPES,
|
scope: SCOPES,
|
||||||
|
// client_secret,
|
||||||
|
// code_verifier,
|
||||||
});
|
});
|
||||||
|
if (client_secret) {
|
||||||
|
params.append('client_secret', client_secret);
|
||||||
|
}
|
||||||
|
if (code_verifier) {
|
||||||
|
params.append('code_verifier', code_verifier);
|
||||||
|
}
|
||||||
const tokenResponse = await fetch(`https://${instanceURL}/oauth/token`, {
|
const tokenResponse = await fetch(`https://${instanceURL}/oauth/token`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
44
src/utils/oauth-pkce.js
Normal file
44
src/utils/oauth-pkce.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
function dec2hex(dec) {
|
||||||
|
return ('0' + dec.toString(16)).slice(-2);
|
||||||
|
}
|
||||||
|
export function verifier() {
|
||||||
|
var array = new Uint32Array(56 / 2);
|
||||||
|
window.crypto.getRandomValues(array);
|
||||||
|
return Array.from(array, dec2hex).join('');
|
||||||
|
}
|
||||||
|
function sha256(plain) {
|
||||||
|
// returns promise ArrayBuffer
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(plain);
|
||||||
|
return window.crypto.subtle.digest('SHA-256', data);
|
||||||
|
}
|
||||||
|
function base64urlencode(a) {
|
||||||
|
let str = '';
|
||||||
|
const bytes = new Uint8Array(a);
|
||||||
|
const len = bytes.byteLength;
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
str += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||||
|
}
|
||||||
|
export async function generateCodeChallenge(v) {
|
||||||
|
const hashed = await sha256(v);
|
||||||
|
return base64urlencode(hashed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If https://mastodon.social/.well-known/oauth-authorization-server exists, means support PKCE
|
||||||
|
export async function supportsPKCE({ instanceURL }) {
|
||||||
|
if (!instanceURL) return false;
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://${instanceURL}/.well-known/oauth-authorization-server`,
|
||||||
|
);
|
||||||
|
if (!res.ok || res.status !== 200) return false;
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For debugging
|
||||||
|
window.__generateCodeChallenge = generateCodeChallenge;
|
Loading…
Add table
Reference in a new issue