OAuth PKCE is in.

Reference PR: https://github.com/mastodon/mastodon/pull/31129
This commit is contained in:
Lim Chee Aun 2024-08-27 13:50:03 +08:00
parent 2e6074d794
commit 7c56b64e8a
5 changed files with 138 additions and 28 deletions

View file

@ -324,6 +324,7 @@ function App() {
const clientID = store.sessionCookie.get('clientID');
const clientSecret = store.sessionCookie.get('clientSecret');
const vapidKey = store.sessionCookie.get('vapidKey');
const verifier = store.sessionCookie.get('codeVerifier');
(async () => {
setUIState('loading');
@ -332,18 +333,24 @@ function App() {
client_id: clientID,
client_secret: clientSecret,
code,
code_verifier: verifier || undefined,
});
const client = initClient({ instance: instanceURL, accessToken });
await Promise.allSettled([
initPreferences(client),
initInstance(client, instanceURL),
initAccount(client, instanceURL, accessToken, vapidKey),
]);
initStates();
if (accessToken) {
const client = initClient({ instance: instanceURL, accessToken });
await Promise.allSettled([
initPreferences(client),
initInstance(client, instanceURL),
initAccount(client, instanceURL, accessToken, vapidKey),
]);
initStates();
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
setIsLoggedIn(true);
setUIState('default');
setIsLoggedIn(true);
setUIState('default');
} else {
setUIState('error');
}
})();
} else {
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
@ -387,6 +394,11 @@ function App() {
setUIState('default');
}
}
// Cleanup
store.sessionCookie.del('clientID');
store.sessionCookie.del('clientSecret');
store.sessionCookie.del('codeVerifier');
}, []);
let location = useLocation();

View file

@ -1337,7 +1337,7 @@ msgid "Accounts…"
msgstr ""
#: src/components/nav-menu.jsx:363
#: src/pages/login.jsx:142
#: src/pages/login.jsx:166
#: src/pages/status.jsx:792
#: src/pages/welcome.jsx:64
msgid "Log in"
@ -1733,7 +1733,7 @@ msgstr ""
#: src/components/shortcuts-settings.jsx:75
#: src/components/shortcuts-settings.jsx:84
#: src/components/shortcuts-settings.jsx:122
#: src/pages/login.jsx:146
#: src/pages/login.jsx:170
msgid "Instance"
msgstr ""
@ -2353,7 +2353,7 @@ msgstr "Login required."
#: src/compose.jsx:90
#: src/pages/http-route.jsx:91
#: src/pages/login.jsx:223
#: src/pages/login.jsx:247
msgid "Go home"
msgstr ""
@ -3025,23 +3025,28 @@ msgstr ""
msgid "No lists yet."
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”"
msgstr ""
#: src/pages/login.jsx:196
#: src/pages/login.jsx:220
msgid "Failed to log in. Please try again or try another instance."
msgstr ""
#: src/pages/login.jsx:208
#: src/pages/login.jsx:232
msgid "Continue with {selectedInstanceText}"
msgstr ""
#: src/pages/login.jsx:209
#: src/pages/login.jsx:233
msgid "Continue"
msgstr ""
#: src/pages/login.jsx:217
#: src/pages/login.jsx:241
msgid "Don't have an account? Create one!"
msgstr ""

View file

@ -11,7 +11,12 @@ import LangSelector from '../components/lang-selector';
import Link from '../components/link';
import Loader from '../components/loader';
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 useTitle from '../utils/useTitle';
@ -63,17 +68,36 @@ function Login() {
instanceURL,
});
if (client_id && client_secret) {
store.sessionCookie.set('clientID', client_id);
store.sessionCookie.set('clientSecret', client_secret);
store.sessionCookie.set('vapidKey', vapid_key);
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);
location.href = await getAuthorizationURL({
instanceURL,
client_id,
});
const [url, verifier] = await getPKCEAuthorizationURL({
instanceURL,
client_id,
});
store.sessionCookie.set('codeVerifier', verifier);
location.href = url;
} else {
alert(t`Failed to register application`);
}
} else {
alert('Failed to register application');
if (client_id && client_secret) {
store.sessionCookie.set('clientID', client_id);
store.sessionCookie.set('clientSecret', client_secret);
store.sessionCookie.set('vapidKey', vapid_key);
location.href = await getAuthorizationURL({
instanceURL,
client_id,
});
} else {
alert(t`Failed to register application`);
}
}
setUIState('default');
} catch (e) {

View file

@ -1,3 +1,5 @@
import { generateCodeChallenge, verifier } from './oauth-pkce';
const { PHANPY_CLIENT_NAME: CLIENT_NAME, PHANPY_WEBSITE: WEBSITE } = import.meta
.env;
@ -25,6 +27,21 @@ export async function registerApplication({ instanceURL }) {
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 }) {
const authorizationParams = new URLSearchParams({
client_id,
@ -42,15 +59,23 @@ export async function getAccessToken({
client_id,
client_secret,
code,
code_verifier,
}) {
const params = new URLSearchParams({
client_id,
client_secret,
redirect_uri: location.origin + location.pathname,
grant_type: 'authorization_code',
code,
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`, {
method: 'POST',
headers: {

44
src/utils/oauth-pkce.js Normal file
View 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;