Add support for SASL EXTERNAL

Can be useful when the server is using e.g. a cookie for
authentication purposes.
This commit is contained in:
Simon Ser 2021-10-12 17:29:56 +02:00
parent a890665775
commit 21a4a71542
4 changed files with 22 additions and 7 deletions

View File

@ -87,8 +87,8 @@ gamja default settings can be set using a `config.json` file at the root:
"autojoin": "#gamja", "autojoin": "#gamja",
// Controls how the password UI is presented to the user. Set to // Controls how the password UI is presented to the user. Set to
// "mandatory" to require a password, "optional" to accept one but not // "mandatory" to require a password, "optional" to accept one but not
// require it, and "disabled" to never ask for a password. Defaults to // require it, "disabled" to never ask for a password, or "external" to
// "optional". // use SASL EXTERNAL. Defaults to "optional".
"auth": "optional", "auth": "optional",
// Default nickname (string). // Default nickname (string).
"nick": "asdf", "nick": "asdf",

View File

@ -124,6 +124,7 @@ export default class App extends Component {
realname: null, realname: null,
nick: null, nick: null,
saslPlain: null, saslPlain: null,
saslExternal: false,
autoconnect: false, autoconnect: false,
autojoin: [], autojoin: [],
}, },
@ -207,6 +208,9 @@ export default class App extends Component {
if (typeof config.server.autoconnect === "boolean") { if (typeof config.server.autoconnect === "boolean") {
connectParams.autoconnect = config.server.autoconnect; connectParams.autoconnect = config.server.autoconnect;
} }
if (config.server.auth === "external") {
connectParams.saslExternal = true;
}
} }
let autoconnect = store.autoconnect.load(); let autoconnect = store.autoconnect.load();

View File

@ -61,6 +61,8 @@ export default class ConnectForm extends Component {
username: params.username || params.nick, username: params.username || params.nick,
password: this.state.password, password: this.state.password,
}; };
} else if (this.props.auth === "external") {
params.saslExternal = true;
} }
this.state.autojoin.split(",").forEach(function(ch) { this.state.autojoin.split(",").forEach(function(ch) {
@ -112,7 +114,7 @@ export default class ConnectForm extends Component {
} }
let auth = null; let auth = null;
if (this.props.auth !== "disabled") { if (this.props.auth !== "disabled" && this.props.auth !== "external") {
auth = html` auth = html`
<label> <label>
Password:<br/> Password:<br/>

View File

@ -69,6 +69,7 @@ export default class Client extends EventTarget {
nick: null, nick: null,
pass: null, pass: null,
saslPlain: null, saslPlain: null,
saslExternal: false,
bouncerNetwork: null, bouncerNetwork: null,
}; };
batches = new Map(); batches = new Map();
@ -498,7 +499,7 @@ export default class Client extends EventTarget {
let reqCaps = []; let reqCaps = [];
let capEnd = true; let capEnd = true;
if (this.params.saslPlain && this.supportsSASL("PLAIN")) { if ((this.params.saslPlain && this.supportsSASL("PLAIN")) || (this.params.saslExternal && this.supportsSASL("EXTERNAL"))) {
// CAP END is deferred after authentication finishes // CAP END is deferred after authentication finishes
reqCaps.push("sasl"); reqCaps.push("sasl");
capEnd = false; capEnd = false;
@ -537,6 +538,9 @@ export default class Client extends EventTarget {
if (cap == "sasl" && this.params.saslPlain) { if (cap == "sasl" && this.params.saslPlain) {
console.log("Starting SASL PLAIN authentication"); console.log("Starting SASL PLAIN authentication");
this.send({ command: "AUTHENTICATE", params: ["PLAIN"] }); this.send({ command: "AUTHENTICATE", params: ["PLAIN"] });
} else if (cap == "sasl" && this.params.saslExternal) {
console.log("Starting SASL EXTERNAL authentication");
this.send({ command: "AUTHENTICATE", params: ["EXTERNAL"] });
} }
}); });
break; break;
@ -552,15 +556,20 @@ export default class Client extends EventTarget {
handleAuthenticate(msg) { handleAuthenticate(msg) {
let challengeStr = msg.params[0]; let challengeStr = msg.params[0];
// For now only PLAIN is supported
if (challengeStr != "+") { if (challengeStr != "+") {
this.dispatchEvent(new CustomEvent("error", { detail: "Expected an empty challenge, got: " + challengeStr })); this.dispatchEvent(new CustomEvent("error", { detail: "Expected an empty challenge, got: " + challengeStr }));
this.send({ command: "AUTHENTICATE", params: ["*"] }); this.send({ command: "AUTHENTICATE", params: ["*"] });
return; return;
} }
if (this.params.saslPlain) {
let respStr = btoa("\0" + this.params.saslPlain.username + "\0" + this.params.saslPlain.password); let respStr = btoa("\0" + this.params.saslPlain.username + "\0" + this.params.saslPlain.password);
this.send({ command: "AUTHENTICATE", params: [respStr] }); this.send({ command: "AUTHENTICATE", params: [respStr] });
} else if (this.params.saslExternal) {
this.send({ command: "AUTHENTICATE", params: [btoa("")] });
} else {
throw new Error("Received AUTHENTICATE for unknown mechanism");
}
} }
send(msg) { send(msg) {