Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OIDC Auth Provider #59

Merged
merged 1 commit into from
Oct 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 109 additions & 5 deletions app/elements/login-form.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@
timeout="30000">
</iron-ajax>

<iron-ajax id="oidcReq"
url="{{oidcURL}}"
handle-as="json"
method="POST"
body="{{oidcBody}}"
content-type="application/json"
last-response="{{oidcResponse}}"
last-error={{loginError}}
on-response="_oidcSuccess"
on-error="_error"
timeout="10000">
</iron-ajax>

<iron-ajax id="testReq"
url="{{testURL}}"
handle-as="json"
Expand All @@ -114,6 +127,7 @@
<paper-tab>LDAP</paper-tab>
<paper-tab>Token</paper-tab>
<paper-tab>User/Pass</paper-tab>
<paper-tab>OIDC</paper-tab>
</paper-tabs>
<div class="flexchild" style="margin: 0;">
<paper-dropdown-menu label="URL" value="{{url}}" horizontal-align="left" style="width: 100%;">
Expand Down Expand Up @@ -153,6 +167,10 @@
<paper-input id="userfield" value="{{username}}" label="Username" disabled="{{loading}}"></paper-input>
<paper-input id="passfield" value="{{password}}" label="Password" type="password" disabled="{{loading}}"></paper-input>
</div>
<div>
<iron-a11y-keys target="[[targetrole]]" keys="enter" on-keys-pressed="_login"></iron-a11y-keys>
<paper-input id="rolefield" value="{{role}}" label="Role (optional)" disabled="{{loading}}"></paper-input>
</div>
</iron-pages>
<div class="buttons">
<paper-button on-tap="_login" autofocus disabled="{{loading}}">Login</paper-button>
Expand All @@ -172,6 +190,10 @@
<iron-icon prefix icon="error-outline" style="padding-right: 7px;"></iron-icon>
Caution! Connecting to Vault over unencrypted HTTP may expose secrets!
</paper-toast>
<paper-toast id="oidctoast" class="fit-bottom" duration="0">
<iron-icon prefix icon="error-outline" style="padding-right: 7px;"></iron-icon>
{{oidcErrorText}}
</paper-toast>
</template>

<script>
Expand All @@ -192,6 +214,9 @@
targetpass: {
value: function() { return this.$.passfield; }
},
targetrole: {
value: function() { return this.$.rolefield; }
},
targetuserldap: {
value: function() { return this.$.userfieldldap; }
},
Expand All @@ -213,6 +238,10 @@
password: String,
authURL: String,
listMountsURL: String,
role: {
type: String,
value: ''
},
backends: {
type: Array,
value: [],
Expand Down Expand Up @@ -269,17 +298,45 @@
approvedURL: {
type: Boolean,
value: false
},
oidcStarted: {
type: Boolean,
value: false
}
},
attached: function() {
this.status = 'none';

// OIDC Auth Setup
// This uses app/preload.js to safely pass data from Electron's main process,
// which is responsible for receiving the callback data from the OIDC provider.
window.onOIDCAuthData(function(data) {
this._oidcAuth(data);
}.bind(this));
window.onOIDCAuthError(function(data) {
this._oidcAuthError(data);
}.bind(this));
window.onOIDCAuthStartError(function(data) {
this._oidcAuthStartError(data);
}.bind(this));
window.onOIDCAuthStartSuccess(function(data) {
this._oidcAuthStartSuccess(data);
}.bind(this));
window.startOIDCServer()
},
_autofocus: function() {
// Close OIDC alert on all pages. Open selectively as needed.
this.$.oidctoast.close();

// Set cursor autofocus for login/password fields
// TODO: autofocus on username field first if unset
if (this.page === 1) this.$.tokenfield.autofocus = true;
else if (this.page === 0) this.$.passfieldldap.autofocus = true;
else if (this.page === 2) this.$.passfield.autofocus = true;
else if (this.page === 3) {
this.$.rolefield.autofocus = true;
if (!(this.oidcStarted)) this.$.oidctoast.open();
}
},
_computeBody: function(p) {
return {"password": p };
Expand All @@ -288,7 +345,7 @@
return this.url + 'v1/auth/ldap/login/' + u
},
_login: function() {
// Switch for LDAP, Token, and UserPass auth backends
// Switch for LDAP, Token, UserPass, and OIDC auth backends
if (this.page == 0) {
if (!this.username && !this.password) { //Check fields have content
this.errorText = 'Username and Password are required';
Expand Down Expand Up @@ -320,9 +377,22 @@
this.authMethod = 'POST';
this.authURL = this.url + 'v1/auth/userpass/login/' + this.username;
this.body = {"password": this.password };
} else if (this.page == 3) {
if (this.oidcStarted) {
this.oidcURL = this.url + 'v1/auth/oidc/oidc/auth_url'
this.oidcBody = {"redirect_uri": "http:https://localhost:8250/oidc/callback", "role": this.role}
this.loading = true;
this.$.oidcReq.generateRequest();
} else {
this.errorText = 'OIDC provider unavailable. A local server to catch the OIDC callback could not be started.';
this.$.errortoast.show();
}
}

if (this.page != 3) { // For OIDC there is a pre-auth step. This is run later.
this.loading = true;
this.push('authRequests', this.$.testReq.generateRequest());
}
this.loading = true;
this.push('authRequests', this.$.testReq.generateRequest());
},
_success: function() {
this.$.errortoast.close()
Expand All @@ -340,18 +410,21 @@
this.token = '';
document.getElementById('blocker').style.display = 'none';
this.$.modal.close();
window.stopOIDCServer();
this.oidcStarted = false;
this.$.oidctoast.close();
},
_error: function(e) {
this.loading = false;
if (this.loginError.response.errors[0].includes('Invalid Credentials')) this.errorText = 'Invalid Credentials';
else if (this.loginError.response.errors[0].includes('No Such Object')) this.errorText = 'Invalid Username or Password';
else if (this.loginError.response.errors[0] == 'invalid username or password') this.errorText = 'Invalid Username or Password';
else if (this.loginError.response.errors[0] == 'missing client token') this.errorText = 'Permission Denied';
else if (this.loginError.response.errors[0] == 'missing client token') this.errorText = 'Permission Denied: missing client token';
else if (this.loginError.response.errors[0] == 'Vault is sealed') this.errorText = 'This vault is currently sealed';
else if (this.loginError.response.errors[0] == 'unsupported path') this.errorText = 'This auth backend is not supported by the server';
else if (this.loginError.response.errors[0] == 'permission denied' && this.page == 1) this.errorText = 'Invalid Token';
else if (this.loginError.response.errors[0] == 'permission denied') this.errorText = 'Permission Denied';
else this.errorText = 'Unknown Error: Please try again later.';
else this.errorText = 'Error from remote server: ' + this.loginError.response.errors[0];
this.$.errortoast.show();
},
_listMounts: function() {
Expand Down Expand Up @@ -449,6 +522,37 @@
_stop: function(event) {
event.stopPropagation();
},
_oidcSuccess: function() {
this.$.errortoast.close()
if (this.oidcResponse.data && this.oidcResponse.data.auth_url) {
window.openOIDCURL(this.oidcResponse.data.auth_url)
} else {
this.loading = false;
this.errorText = 'Unknown Error. It is possible that "http:https://localhost:8250/oidc/callback" is not an allowed callback URL in the OIDC configuration.';
this.$.errortoast.show();
}
},
_oidcAuth: function(data) {
this.authMethod = 'GET';
this.authURL = this.url + 'v1/auth/oidc/oidc/callback?code=' + data.code + '&state=' + data.state;
this.body = '';
this.header = '';
this.push('authRequests', this.$.testReq.generateRequest());
},
_oidcAuthError: function(data) {
this.loading = false;
this.errorText = 'Error while capturing OIDC auth flow: ' + data;
this.$.errortoast.show();
},
_oidcAuthStartError: function(data) {
this.oidcStarted = false;
this.oidcErrorText = data;
if (this.page === 3) this.$.oidctoast.show();
},
_oidcAuthStartSuccess: function(data) {
this.oidcStarted = true;
this.$.oidctoast.close();
},
_watchAuthRequests: function() {
if (this.authRequests[0]) {
this.$.authenticateReq.generateRequest();
Expand Down
1 change: 1 addition & 0 deletions app/elements/login-status.html
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ <h2>Logging Out</h2>
document.getElementById('blocker').style.display = 'block';
clearInterval(this.timer);
this.$.renewTokenModal.close();
window.startOIDCServer()
},
_clearData: function() {
app.data = {};
Expand Down
3 changes: 3 additions & 0 deletions app/elements/secrets-init.html
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@
if (this.loginResponse.identity_policies){
policies = policies.concat(this.loginResponse.identity_policies);
}
policies = [...new Set(policies)]
}
for (var i = 0; i < policies.length; i++) {
// Filter policies definitions from secret access definitions
Expand Down Expand Up @@ -316,6 +317,7 @@
if (this.loginResponse.identity_policies){
policies = policies.concat(this.loginResponse.identity_policies);
}
policies = [...new Set(policies)]
if (this.policyRequests.length == policies.length) {
var complete = true;
for (var i = 0; i < this.policyRequests.length; i++)
Expand All @@ -341,6 +343,7 @@
policies = policies.concat(this.loginResponse.identity_policies);
}
}
policies = [...new Set(policies)]
if (policies.length > 0) {
this.loading = true;
for (var i = 0; i < policies.length; i++) {
Expand Down
71 changes: 70 additions & 1 deletion app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,42 @@ limitations under the License.
const electron = require('electron');
const {Menu} = require('electron');
const {app} = require('electron');
const {ipcMain} = require('electron')
const {shell} = require('electron');
const http = require("http");
const url = require('url');
const path = require('path')
const windowStateKeeper = require('electron-window-state');
const BrowserWindow = electron.BrowserWindow;

let mainWindow;
var timer;

var server = http.createServer(function (req, res) {
var url_obj = url.parse(req.url, true);
if (url_obj.pathname === '/oidc/callback') {
var query_parameters = url_obj.query;
var fail = false;

if ((JSON.stringify(query_parameters) === '{}') ||
(!('state' in query_parameters)) ||
(!('code' in query_parameters)) ||
(query_parameters['state'] == "") ||
(query_parameters['code'] == "")) fail = true;

if (fail) {
mainWindow.webContents.send('oidc-auth-error', 'OIDC provider returned an unexpected result (empty required params)');
res.end();
} else {
mainWindow.webContents.send('oidc-auth-data', query_parameters);
}

res.writeHead(200);
res.end("<html>success <script>window.close()</script></html>");
} else {
res.end();
}
});

function createWindow() {
let mainWindowState = windowStateKeeper({
Expand All @@ -41,7 +73,9 @@ function createWindow() {
'webPreferences': {
'nodeIntegration': false,
'sandbox': true,
'disableBlinkFeatures': 'Auxclick'
'disableBlinkFeatures': 'Auxclick',
'contextIsolation': false,
'preload': path.join(__dirname, 'preload.js')
}
});

Expand Down Expand Up @@ -80,14 +114,49 @@ function createWindow() {
// Emitted when the window is closed.
mainWindow.on('closed', function() {
mainWindow = null;
server.close()
app.quit();
});

mainWindow.webContents.on('will-navigate', function(e){
e.preventDefault();
});

// OIDC server: on failure to start, retry and report error to frontend
server.on('error', (e) => {
mainWindow.webContents.send('oidc-auth-start-error', 'OIDC Auth: Unable to start web server to capture OIDC login callback. Error code: ' + e.code + '. Retrying in 10 seconds.');
server.close();
timer = setTimeout(() => {
startServer()
}, 10000);
});

// IPC to support OIDC auth flow
ipcMain.on("start-oidc", startServer);
ipcMain.on("stop-oidc", function (ev) {
server.close();
clearTimeout(timer);
});
ipcMain.on("open-oidc-url", function (ev, location) {
if (!['https:', 'http:'].includes(url.parse(location).protocol)) return;
shell.openExternal(location);
});
}

function startServer() {
console.log(server.listening)
if (server.listening != true) {
server.listen({
host: '127.0.0.1',
port: 8250,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the server port should be configurable in case it could collide with something?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR; Cryptr has been written with the YAGNI principle in mind.

This is unfortunately a complex problem. If the server:port combination used by Cryptr is not also in the approved callback URLs list configured in the OIDC provider, the OIDC provider will cancel the login attempt and produce an error.

By default, Vault CLI uses http:https://127.0.0.1:8250 as its callback URL, and as such, this URL has the highest likelihood of already being in the OIDC provider's allowlist. For this reason this server:port combination is hardcoded here. Should sufficient need arise for customizing this port, a request can be evaluated.

To avoid port conflicts, Cryptr listens on 127.0.0.1:8250 for as short a time as possible. Error handling and retries have been added to ensure Cryptr owns (and is the sole listener on) the port before allowing the OIDC auth flow to proceed. A quick search indicates that few other pieces of software specifically claim this port, so the likelihood of always-on software claiming this port is extremely low.

exclusive: true
}, function() {
mainWindow.webContents.send('oidc-auth-start-success', '');
});
}
}


// This method will be called when Electron has finished initialization and is ready to create browser windows.
app.on('ready', createWindow);

Expand Down
46 changes: 46 additions & 0 deletions app/preload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const { ipcRenderer } = require('electron')

window.startOIDCServer = function() {
ipcRenderer.send("start-oidc", "start");
}

window.stopOIDCServer = function() {
ipcRenderer.send("stop-oidc", "stop");
}

window.openOIDCURL = function(url) {
ipcRenderer.send("open-oidc-url", url);
}


window.onOIDCAuthData = function(callback) {
authData = callback
}

window.onOIDCAuthError = function(callback) {
authError = callback
}

window.onOIDCAuthStartError = function(callback) {
authStartError = callback
}

window.onOIDCAuthStartSuccess = function(callback) {
authStartSuccess = callback
}

ipcRenderer.on('oidc-auth-data', (event, arg) => {
if (authData) authData(arg);
});

ipcRenderer.on('oidc-auth-error', (event, arg) => {
if (authError) authError(arg);
});

ipcRenderer.on('oidc-auth-start-error', (event, arg) => {
if (authStartError) authStartError(arg);
});

ipcRenderer.on('oidc-auth-start-success', (event, arg) => {
if (authStartSuccess) authStartSuccess(arg);
});