Add support for OAuth 2.0 authentication

This commit is contained in:
Simon Ser 2022-09-12 11:38:43 +02:00
parent bbc94c88c0
commit e815295503
5 changed files with 228 additions and 4 deletions

View file

@ -1,5 +1,6 @@
import * as irc from "../lib/irc.js";
import Client from "../lib/client.js";
import * as oauth2 from "../lib/oauth2.js";
import Buffer from "./buffer.js";
import BufferList from "./buffer-list.js";
import BufferHeader from "./buffer-header.js";
@ -249,7 +250,7 @@ export default class App extends Component {
* - Default server URL constructed from the current URL location (this is
* done in fillConnectParams)
*/
handleConfig(config) {
async handleConfig(config) {
let connectParams = { ...this.state.connectParams };
if (typeof config.server.url === "string") {
@ -277,6 +278,10 @@ export default class App extends Component {
console.error("Error in config.json: cannot set server.autoconnect = true and server.auth = \"mandatory\"");
connectParams.autoconnect = false;
}
if (config.server.auth === "oauth2" && (!config.oauth2 || !config.oauth2.url || !config.oauth2.client_id)) {
console.error("Error in config.json: server.auth = \"oauth2\" requires oauth2 settings");
config.server.auth = null;
}
let autoconnect = store.autoconnect.load();
if (autoconnect) {
@ -329,6 +334,40 @@ export default class App extends Component {
connectParams.nick = connectParams.nick.replace("*", placeholder);
}
if (config.server.auth === "oauth2" && !connectParams.saslOauthBearer) {
if (queryParams.error) {
console.error("OAuth 2.0 authorization failed: ", queryParams.error);
this.showError("Authentication failed: " + (queryParams.error_description || queryParams.error));
return;
}
if (!queryParams.code) {
this.redirectOauth2Authorize();
return;
}
// Strip code from query params, to prevent page refreshes from
// trying to exchange the code again
let url = new URL(window.location.toString());
url.searchParams.delete("code");
url.searchParams.delete("state");
window.history.replaceState(null, "", url.toString());
let saslOauthBearer;
try {
saslOauthBearer = await this.exchangeOauth2Code(queryParams.code);
} catch (err) {
this.showError(err);
return;
}
connectParams.saslOauthBearer = saslOauthBearer;
if (saslOauthBearer.username && !connectParams.nick) {
connectParams.nick = saslOauthBearer.username;
}
}
if (autojoin.length > 0) {
if (connectParams.autoconnect) {
// Ask the user whether they want to join that new channel.
@ -347,6 +386,59 @@ export default class App extends Component {
}
}
async redirectOauth2Authorize() {
let serverMetadata;
try {
serverMetadata = await oauth2.fetchServerMetadata(this.config.oauth2.url);
} catch (err) {
console.error("Failed to fetch OAuth 2.0 server metadata:", err);
this.showError("Failed to fetch OAuth 2.0 server metadata");
}
oauth2.redirectAuthorize({
serverMetadata,
clientId: this.config.oauth2.client_id,
redirectUri: window.location.toString(),
scope: this.config.oauth2.scope,
});
}
async exchangeOauth2Code(code) {
let serverMetadata = await oauth2.fetchServerMetadata(this.config.oauth2.url);
let redirectUri = new URL(window.location.toString());
redirectUri.searchParams.delete("code");
redirectUri.searchParams.delete("state");
let data = await oauth2.exchangeCode({
serverMetadata,
redirectUri: redirectUri.toString(),
code,
clientId: this.config.oauth2.client_id,
clientSecret: this.config.oauth2.client_secret,
});
// TODO: handle expires_in/refresh_token
let token = data.access_token;
let username = null;
if (serverMetadata.introspection_endpoint) {
try {
let data = await oauth2.introspectToken({
serverMetadata,
token,
clientId: this.config.oauth2.client_id,
clientSecret: this.config.oauth2.client_secret,
});
username = data.username;
} catch (err) {
console.warn("Failed to introspect OAuth 2.0 token:", err);
}
}
return { token, username };
}
showError(err) {
console.error("App error: ", err);