Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d576395
Add new route w/ controller oidc-provider
hashishaw Sep 10, 2021
efb4aeb
oidc-provider controller has params, template has success message (te…
hashishaw Sep 28, 2021
fb340e2
Move oidc-provider route to under identity
hashishaw Sep 29, 2021
3f38ad2
Do not redirect after poll if on oidc-provider page
hashishaw Oct 6, 2021
36a2366
WIP provider -- beforeModel handles prompt, logout, redirect
hashishaw Oct 6, 2021
50ba8cf
Auth service fetch method rejects with fetch response if status >= 300
hashishaw Oct 6, 2021
fb97e47
New component OidcConsentBlock
hashishaw Oct 11, 2021
bfc9c4b
Fix redirect to/from auth with cluster name, show error and consent f…
hashishaw Oct 11, 2021
6f4b3c3
Show error and consent form on template
hashishaw Oct 11, 2021
0f7f420
Add component test, update docs
hashishaw Oct 11, 2021
1080fba
Test for oidc-consent-block component
hashishaw Oct 11, 2021
b335a3c
Add changelog
hashishaw Oct 11, 2021
eb484be
fix tests
hashishaw Oct 11, 2021
b86216a
Add authorize to end of router path
hashishaw Oct 11, 2021
d87db7b
Remove unused tests
hashishaw Oct 11, 2021
e360231
Update changelog with feature name
hashishaw Oct 11, 2021
4c73c5c
Add descriptions for OidcConsentBlock component
hashishaw Oct 11, 2021
2d96955
glimmerize token-expire-warning and don't override yield if on oidc-p…
hashishaw Oct 13, 2021
17f2d60
remove text on token-expire-warning
hashishaw Oct 13, 2021
9582713
Fix null transition.to on cluster redirect
hashishaw Oct 13, 2021
f8f5591
Hide nav links if oidc-provider route
hashishaw Oct 13, 2021
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
3 changes: 3 additions & 0 deletions changelog/12800.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
ui: OIDC Authorization Code Flow Support
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this the new format that Meggie is requesting for features?

```
59 changes: 59 additions & 0 deletions ui/app/components/oidc-consent-block.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @module OidcConsentBlock
* OidcConsentBlock components are used to...
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

fill in?

*
* @example
* ```js
* <OidcConsentBlock @requiredParam={requiredParam} @optionalParam={optionalParam} @param1={{param1}}/>
* ```
* @param {string} redirect - redirect is the URL where successful consent will redirect to
* @param {string} code - code is the string required to pass back to redirect on successful OIDC auth
* @param {string} [state] - state is a string which is required to return on redirect if provided, but optional generally
*/

import Ember from 'ember';
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';

const validParameters = ['code', 'state'];
export default class OidcConsentBlockComponent extends Component {
@tracked didCancel = false;

get win() {
return this.window || window;
}

buildUrl(urlString, params) {
try {
let url = new URL(urlString);
Object.keys(params).forEach(key => {
if (params[key] && validParameters.includes(key)) {
url.searchParams.append(key, params[key]);
}
});
return url;
} catch (e) {
console.debug('DEBUG: parsing url failed for', urlString);
throw new Error('Invalid URL');
}
}

@action
handleSubmit(evt) {
evt.preventDefault();
let { redirect, ...params } = this.args;
let redirectUrl = this.buildUrl(redirect, params);
if (Ember.testing) {
this.args.testRedirect(redirectUrl.toString());
} else {
this.win.location.replace(redirectUrl);
}
}

@action
handleCancel(evt) {
evt.preventDefault();
this.didCancel = true;
}
}
24 changes: 24 additions & 0 deletions ui/app/controllers/vault/cluster/identity/oidc-provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Controller from '@ember/controller';

export default class VaultClusterIdentityOidcProviderController extends Controller {
queryParams = [
'scope', // *
'response_type', // *
'client_id', // *
'redirect_uri', // *
'state', // *
'nonce', // *
'display',
'prompt',
'max_age',
];
scope = null;
response_type = null;
client_id = null;
redirect_uri = null;
state = null;
nonce = null;
display = null;
prompt = null;
max_age = null;
}
5 changes: 4 additions & 1 deletion ui/app/mixins/cluster-route.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const AUTH = 'vault.cluster.auth';
const CLUSTER = 'vault.cluster';
const CLUSTER_INDEX = 'vault.cluster.index';
const OIDC_CALLBACK = 'vault.cluster.oidc-callback';
const OIDC_PROVIDER = 'vault.cluster.identity.oidc-provider';
const DR_REPLICATION_SECONDARY = 'vault.cluster.replication-dr-promote';
const DR_REPLICATION_SECONDARY_DETAILS = 'vault.cluster.replication-dr-promote.details';
const EXCLUDED_REDIRECT_URLS = ['/vault/logout'];
Expand All @@ -20,7 +21,9 @@ export default Mixin.create({

transitionToTargetRoute(transition = {}) {
const targetRoute = this.targetRouteName(transition);

if (OIDC_PROVIDER === this.router.currentRouteName) {
return RSVP.resolve();
}
if (
targetRoute &&
targetRoute !== this.routeName &&
Expand Down
4 changes: 4 additions & 0 deletions ui/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ Router.map(function() {
}

this.route('not-found', { path: '/*path' });

this.route('identity', function() {
this.route('oidc-provider', { path: '/oidc/provider/:oidc_name' });
});
});
this.route('not-found', { path: '/*path' });
});
Expand Down
115 changes: 115 additions & 0 deletions ui/app/routes/vault/cluster/identity/oidc-provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

const AUTH = 'vault.cluster.auth';
const PROVIDER = 'vault.cluster.identity.oidc-provider';

export default class VaultClusterIdentityOidcProviderRoute extends Route {
@service auth;
@service router;

get win() {
return this.window || window;
}

_redirect(url, params) {
let redir = this._buildUrl(url, params);
this.win.location.replace(redir);
}

beforeModel(transition) {
const currentToken = this.auth.get('currentTokenName');
let { redirect_to, ...qp } = transition.to.queryParams;
console.debug('DEBUG: removing redirect_to', redirect_to);
if (!currentToken && 'none' === qp.prompt?.toLowerCase()) {
this._redirect(qp.redirect_uri, {
state: qp.state,
error: 'login_required',
});
} else if (!currentToken || 'login' === qp.prompt?.toLowerCase()) {
if ('login' === qp.prompt?.toLowerCase()) {
this.auth.deleteCurrentToken();
qp.prompt = null;
}
let { cluster_name } = this.paramsFor('vault.cluster');
let url = this.router.urlFor(transition.to.name, transition.to.params, { queryParams: qp });
return this.transitionTo(AUTH, cluster_name, { queryParams: { redirect_to: url } });
}
}

_redirectToAuth(oidcName, queryParams, logout = false) {
let { cluster_name } = this.paramsFor('vault.cluster');
let currentRoute = this.router.urlFor(PROVIDER, oidcName, { queryParams });
if (logout) {
this.auth.deleteCurrentToken();
}
return this.transitionTo(AUTH, cluster_name, { queryParams: { redirect_to: currentRoute } });
}

_buildUrl(urlString, params) {
try {
let url = new URL(urlString);
Object.keys(params).forEach(key => {
if (params[key]) {
url.searchParams.append(key, params[key]);
}
});
return url;
} catch (e) {
console.debug('DEBUG: parsing url failed for', urlString);
throw new Error('Invalid URL');
}
}

_handleSuccess(response, baseUrl, state) {
const { code } = response;
let redirectUrl = this._buildUrl(baseUrl, { code, state });
this.win.location.replace(redirectUrl);
}
_handleError(errorResp, baseUrl) {
let redirectUrl = this._buildUrl(baseUrl, { ...errorResp });
this.win.location.replace(redirectUrl);
}

async model(params) {
let { oidc_name, ...qp } = params;
let decodedRedirect = decodeURI(qp.redirect_uri);
let url = this._buildUrl(`${this.win.origin}/v1/identity/oidc/provider/${oidc_name}/authorize`, qp);
try {
const response = await this.auth.ajax(url, 'GET', {});
if ('consent' === qp.prompt?.toLowerCase()) {
return {
consent: {
code: response.code,
redirect: decodedRedirect,
state: qp.state,
},
};
}
this._handleSuccess(response, decodedRedirect, qp.state);
} catch (errorRes) {
let resp = await errorRes.json();
let code = resp.error;
if (code === 'max_age_violation') {
this._redirectToAuth(oidc_name, qp, true);
} else if (code === 'invalid_redirect_uri') {
return {
error: {
title: 'Redirect URI mismatch',
message:
'The provided redirect_uri is not in the list of allowed redirect URIs. Please make sure you are sending a valid redirect URI from your application.',
},
};
} else if (code === 'invalid_client_id') {
return {
error: {
title: 'Invalid client ID',
message: 'Your client ID is invalid. Please update your configuration and try again.',
},
};
} else {
this._handleError(resp, decodedRedirect);
}
}
}
}
2 changes: 1 addition & 1 deletion ui/app/services/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export default Service.extend({
} else if (response.status >= 200 && response.status < 300) {
return resolve(response.json());
} else {
return reject();
return reject(response);
}
});
},
Expand Down
23 changes: 23 additions & 0 deletions ui/app/templates/components/oidc-consent-block.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{{#if this.didCancel}}
<h3 class="title is-3" data-test-consent-title>
Consent Not Given
</h3>
<div class="box">
<p class="has-bottom-margin-l has-top-margin-l">Login attempt has been terminated.</p>
</div>
{{else}}
<h3 class="title is-3" data-test-consent-title>
Consent
</h3>
<form class="box" {{on 'submit' this.handleSubmit}} data-test-consent-form>
<p class="has-bottom-margin-s">In order to complete the login process, you must consent to Vault sharing your profile, email, address, and phone with the client.</p>
<p class="has-bottom-margin-s">Do you want to continue?</p>
<FormSaveButtons
@saveButtonText="Yes"
@isSaving={{false}}
@cancelButtonText="No"
@onCancel={{this.handleCancel}}
@includeBox={{false}}
/>
</form>
{{/if}}
25 changes: 25 additions & 0 deletions ui/app/templates/vault/cluster/identity/oidc-provider.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<div class="splash-page-container section is-flex-v-centered-tablet is-flex-1 is-fullwidth">
<div class="columns is-centered is-gapless is-fullwidth">
<div class="column is-4-desktop is-6-tablet">
{{#if model.error}}
<div class="box is-shadowless is-flex-v-centered">
<LogoEdition />
</div>
<AlertBanner
@type="danger"
@title={{model.error.title}}
@message={{model.error.message}}
/>
{{else if model.consent}}
<OidcConsentBlock
@code={{model.consent.code}}
@state={{model.consent.state}}
@redirect={{model.consent.redirect}}
@onSuccess={{this._handleSuccess}}
/>
{{else}}
<VaultLogoSpinner />
{{/if}}
</div>
</div>
</div>
1 change: 1 addition & 0 deletions ui/lib/core/addon/components/form-save-buttons.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import layout from '../templates/components/form-save-buttons';
* ```
*
* @param [saveButtonText="Save" {String}] - The text that will be rendered on the Save button.
* @param [cancelButtonText="Cancel" {String}] - The text that will be rendered on the Cancel button.
* @param [isSaving=false {Boolean}] - If the form is saving, this should be true. This will disable the save button and render a spinner on it;
* @param [cancelLinkParams=[] {Array}] - An array of arguments used to construct a link to navigate back to when the Cancel button is clicked.
* @param [onCancel=null {Fuction}] - If the form should call an action on cancel instead of route somewhere, the fucntion can be passed using onCancel instead of passing an array to cancelLinkParams.
Expand Down
Loading