Commit 17e24cb0 authored by Nicolas Mérigot's avatar Nicolas Mérigot

feature: add multi provider login

parent 495ec912
import SIBAuth from './sib-auth.js';
import SIBAuthProvider from './sib-auth-provider.js';
export {
SIBAuth,
SIBAuthProvider,
};
import 'https://unpkg.com/oidc-client@1.6';
Log.logger = console; // eslint-disable-line no-undef
Log.level = Log.INFO; // eslint-disable-line no-undef
const defaultSettings = {
redirect_uri: window.location.origin,
post_logout_redirect_uri: window.location.origin,
// authority: 'https://test-paris.happy-dev.fr/openid/',
// client_id: '833925',
response_type: 'id_token token',
scope: 'openid profile email',
automaticsilentrenew: 'true',
loaduserinfo: 'true',
};
class SIBAuthProvider extends HTMLElement {
/**
* @typedef {Object} State
* @property {string} value 'login' or 'logout' or null
* @property {token} string CRSF token
* @property {previousUri} string Previous uri
*/
constructor() {
super();
this.manager = null;
this.id = null;
}
/** @function
* @name connectedCallback
* When called, the OIDC manager is intancied
* with the params set in the component, render view
*/
async connectedCallback() {
const { authority, client_id, id } = this.dataset; // eslint-disable-line camelcase
this.id = id;
const settings = Object.assign({}, defaultSettings, { authority, client_id });
this.manager = new UserManager(settings); // eslint-disable-line no-undef
this.render(this.dataset);
}
/** @function
* @name processState
* Try to get user, the if a state is set, call the appropriate method
* If failed, clear state
*/
async processState(parent) {
const user = await this.manager.getUser();
const state = parent.getState();
if (user) {
parent.setUser(user);
}
if (state && state.value) {
try {
switch (state.value) {
case 'login':
this.loginCallback(parent);
break;
case 'logout':
this.logoutCallback(parent);
break;
default:
parent.clearState();
}
} catch (e) {
parent.clearState();
}
}
}
/** @function
* @name disconnectedCallback
* Remove manager
*/
disconnectedCallback() {
this.manager = null;
}
/** @function
* @name login
* Start login procedure
* @param {SIBAuth} parent - SIBAuth parent instance
*/
async login(parent) {
const { token } = parent.setState('login', this.id);
await this.manager.signinRedirect({
state: token,
});
}
/** @function
* @name loginCallback
* Finish the login procedure
* @param {SIBAuth} parent - SIBAuth parent instance
*/
async loginCallback(parent) {
const { token, previousUri } = parent.getState();
const user = await this.manager.signinRedirectCallback();
if (user.state !== token) {
throw new Error('CRSF token doesnt match');
}
parent.setUser(user);
parent.clearState();
window.location.href = previousUri;
}
/** @function
* @name logout
* Start a logout procedure
* @param {SIBAuth} parent - SIBAuth parent instance
*/
async logout(parent) {
const { token } = parent.setState('logout', this.id);
await this.manager.signoutRedirect({
state: token,
});
}
/** @function
* @name logoutCallback
* Finish the logout procedure
* @param {SIBAuth} parent - SIBAuth parent instance
*/
async logoutCallback(parent) {
const { token } = parent.getState();
const signout = await this.manager.signoutRedirectCallback();
if (signout.state !== token) {
throw new Error('CRSF token doesnt match');
}
parent.setUser(null);
parent.clearState();
}
/** @function
* @name render
* Render element
* @param {object} props - Dataset elements props
*/
render({ label, id }) {
this.innerHTML = `
${label || id}
`;
}
}
customElements.define('sib-auth-provider', SIBAuthProvider);
export default SIBAuthProvider;
.sib-auth {
position: absolute;
top:0;
left:0;
height:100vh;
width: 100vw;
background: white;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.sib-auth::before {
content: 'Sélectionner un fournisseur d\'identité';
font-size: 2em;
width: 100vw;
text-align: center;
}
.sib-auth-provider {
border: 1px solid grey;
box-shadow: 4px 4px 8px 0 rgba(0, 0, 0, 0.2);
transition: 0.2s;
width: 10em;
height: 3em;
padding: 2px 14px;
margin: 1em;
}
.sib-auth-provider:hover {
box-shadow: 8px 8px 16px 0 rgba(0, 0, 0, 0.2);
}
import 'https://unpkg.com/oidc-client@1.6';
import { SIBBase } from './core/src/index.js';
Log.logger = console; // eslint-disable-line no-undef
Log.level = Log.INFO; // eslint-disable-line no-undef
const defaultSettings = {
redirect_uri: window.location.origin,
post_logout_redirect_uri: window.location.origin,
// authority: 'https://test-paris.happy-dev.fr/openid/',
// client_id: '833925',
response_type: 'id_token token',
scope: 'openid profile email',
automaticsilentrenew: 'true',
loaduserinfo: 'true',
};
class SIBOidc extends HTMLElement {
import { SIBBase, Helpers } from './core/src/index.js';
class SIBAuth extends HTMLElement {
/**
* @typedef {Object} State
* @property {string} provider auth provider id
* @property {string} value 'login' or 'logout' or null
* @property {token} string CRSF token
* @property {previousUri} string Previous uri
......@@ -26,28 +13,31 @@ class SIBOidc extends HTMLElement {
constructor() {
super();
this.state = null;
this.manager = null;
this.user = null;
// duplicate default settings in this.settings
this.settings = Object.assign({}, defaultSettings);
this.hide();
}
/** @function
* @name connectedCallback
* When called, the OIDC manager is intancied
* with the params set in the component then install method is called
* When called, install method is called
* then we process the current OIDC state
*/
async connectedCallback() {
this.settings = Object.assign({}, defaultSettings, this.dataset);
// initialize OIDC Manager
this.manager = new UserManager(this.settings); // eslint-disable-line no-undef
Helpers.importCSS('./sib-auth.css');
this.install();
this.processState();
}
/** @function
* @name disconnectedCallback
* Clear state and user, uninstall
*/
disconnectedCallback() {
this.setUser();
this.clearState();
this.uninstall();
}
/** @function
* @name install
* Add method and property on SIBBase class in order
......@@ -70,6 +60,52 @@ class SIBOidc extends HTMLElement {
SIBBase.prototype.getUser = null;
}
/** @function
* @name getProvider
* Get provider that match the id
* @param {string} id - the provider id
* @returns {DOMNode} - The provider DOMNode or null
*/
getProvider(id) {
return this.querySelector(`sib-auth-provider[data-id=${id}]`);
}
/** @function
* @name getFavoriteProvider
* Get favorite or default provider
* @returns {DOMNode} - The provider DOMNode or null
*/
getFavoriteProvider() {
const id = localStorage.getItem('oidc_favorite_provider');
let provider = this.getProvider(id);
if (!provider) {
const providers = this.querySelectorAll('sib-auth-provider');
if (providers.length === 1) {
provider = providers.item(0);
}
}
return provider;
}
/** @function
* @name getProviders
* Get all providers
* @returns {DOMNodeList} - The provider DOMNodeList or null
*/
getProviders() {
return this.querySelectorAll('sib-auth-provider');
}
/** @function
* @name setFavoriteProvider
* Set the favorite provider
*/
// eslint-disable-next-line class-methods-use-this
setFavoriteProvider(id) {
localStorage.setItem('oidc_favorite_provider', id);
}
/** @function
* @name getState
* Search in localStorage for previous OIDC state
......@@ -82,6 +118,7 @@ class SIBOidc extends HTMLElement {
this.state = JSON.parse(state);
} else {
this.state = {
provider: null,
value: null,
token: null,
previousUri: null,
......@@ -95,11 +132,13 @@ class SIBOidc extends HTMLElement {
* @name setState
* Set state in localStorage
* @param {string} value - 'login' or 'logout', default null
* @param {string} provider - the id of the provider
* @returns {State} - The state
*/
setState(value = null) {
setState(value = null, provider = null) {
const state = {
value,
provider,
token: (value === null) ? null : Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5),
previousUri: (value === null) ? null : window.location.href,
};
......@@ -112,63 +151,32 @@ class SIBOidc extends HTMLElement {
/** @function
* @name clearState
* Reset current state and clear stale state on manager
* Reset current state
*/
clearState() {
this.manager.clearStaleState();
this.setState();
}
/** @function
* @name processState
* Try to get user, the if a state is set, call the appropriate method
* If failed, clear state
* Try to get user, the if a state is set, call the appropriate provider
*/
async processState() {
const user = await this.manager.getUser();
const state = this.getState();
if (user) {
this.user = user;
return;
const { provider } = this.getState();
const providerElement = this.getProvider(provider);
if (providerElement) {
this.callProvider(providerElement, 'processState', this);
}
if (state && state.value) {
try {
switch (state.value) {
case 'login':
this.loginCallback(state.token);
break;
case 'logout':
this.logoutCallback(state.token);
break;
default:
this.setState();
}
} catch (e) {
this.clearState();
}
}
}
/** @function
* @name disconnectedCallback
* Clear state, remove manager and user, uninstall
*/
disconnectedCallback() {
this.clearState();
this.uninstall();
this.manager = null;
this.user = null;
}
/** @function
* @name dispatchUserInfo
* @param {User} user - User
* Try to replace data-src by user iri on [bind-user] elements
*/
async dispatchUserInfo() {
// eslint-disable-next-line class-methods-use-this
async dispatchUserInfo(user) {
const processDOM = async () => {
const user = await this.getUser();
const id = user.profile.website;
const elements = document.querySelectorAll(`[bind-user]:not([data-src="${id}"])`);
elements.forEach((element) => {
......@@ -184,68 +192,98 @@ class SIBOidc extends HTMLElement {
}
/** @function
* @name login
* Start login procedure
* @name getUser
* Return User or undefined
* @return {User}
*/
async login() {
const { token } = this.setState('login');
await this.manager.signinRedirect({
state: token,
});
getUser() {
if (this.user) {
return this.user;
}
const storedUser = localStorage.getItem('oidc_user');
if (!storedUser) {
return null;
}
this.user = JSON.parse(storedUser);
return this.user;
}
/** @function
* @name loginCallback
* Finish the login procedure
* @name setUser
* Set User
* @param {User} user
*/
async loginCallback() {
const { token, previousUri } = this.getState();
const user = await this.manager.signinRedirectCallback();
if (user.state !== token) {
throw new Error('CRSF token doesnt match');
}
setUser(user) {
localStorage.setItem('oidc_user', JSON.stringify(user));
this.user = user;
this.setState();
window.location.href = previousUri;
}
/** @function
* @name login
* Try to login, if favorite provider is set, trigger login, if not, select provider
*/
async login() {
const favoriteProvider = this.getFavoriteProvider();
if (favoriteProvider) {
this.callProvider(favoriteProvider, 'login', this);
}
this.show();
}
/** @function
* @name logout
* Start a logout procedure
* Try to logout if favorite provider is set
*/
async logout() {
const { token } = this.setState('logout');
await this.manager.signoutRedirect({
state: token,
});
const favoriteProvider = this.getFavoriteProvider();
if (favoriteProvider) {
this.callProvider(favoriteProvider, 'logout', this);
}
}
/** @function
* @name logoutCallback
* Finish the logout procedure
* @name hide
* Hide provider selector
*/
async logoutCallback() {
const { token } = this.getState();
const signout = await this.manager.signoutRedirectCallback();
if (signout.state !== token) {
throw new Error('CRSF token doesnt match');
}
this.setState();
hide() {
this.initialDisplayStyle = this.style.display;
this.style.display = 'none';
this.removeEventListener('click', this.hide);
}
/** @function
* @name getUser
* Return User or undefined
* @return {User}
* @name show
* Show provider selector, bind click event
*/
getUser() {
if (this.user) {
return this.user;
}
return undefined;
show() {
const providers = this.getProviders();
providers.forEach((provider) => {
provider.addEventListener('click', () => {
this.setFavoriteProvider(provider.id);
this.callProvider(provider, 'login', this);
});
});
this.addEventListener('click', this.hide);
this.style.display = this.initialDisplayStyle;
}
/** @function
* @name callProvider
* Call provider method with args
* @param {SIBAuthProvider} provider - auth provider
* @param {string} method - the method to call
* @param args - arguments to pass
*/
// eslint-disable-next-line class-methods-use-this
async callProvider(provider, method, ...args) {
await customElements.whenDefined('sib-auth-provider');
return provider[method](...args);
}
}
customElements.define('sib-auth', SIBOidc);
customElements.define('sib-auth', SIBAuth);
export default SIBOidc;
export default SIBAuth;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment