...
 
Commits (21)
{
"parser": "babel-eslint",
"extends": [
"airbnb-base"
],
"env": {
"browser": true
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
},
"rules": {
"import/no-unresolved": 0,
"import/extensions": [
0,
"always"
],
}
}
---
image: node
before_script:
- npm install
stages:
- test
- release
......@@ -8,7 +11,7 @@ stages:
test:
stage: test
script:
- echo 'Make your tests here !'
- npm run test
except:
- master
tags:
......
# SIB OIDC
Allows your users to login via a given OIDC provider.
Allows your users to login via a given OIDC provider.
## Installation
Add the following within the `<head>` of your HTML:
Add the following within the `<head>` of your HTML:
```html
<script src="https://cdn.happy-dev.fr/oidc-client/oidc-client.min.js"></script>
<script src="https://cdn.happy-dev.fr/sib-oidc/sib-oidc.js"
data-authority="https://some-oidc-provider.gold/openid/"
data-client_id="833925"
data-redirect_uri="http://my-app.gold"
data-response_type="id_token token"
data-scope="openid profile email"
data-automaticSilentRenew="true"
data-loadUserInfo="true"
></script>
<script type="module" src="https://unpkg.com/@startinblox/oidc@latest"></script>
<sib-auth>
<sib-auth-provider
class="sib-auth-provider"
data-authority="https://test-paris.happy-dev.fr/openid/"
data-client_id="833925"
data-id="paris"
>
</sib-auth-provider>
</sib-auth>
```
## Documentation
### bind-user
To associate the currently logged in user to a component, add the `bind-user` attribute to it.
It will set its `data-src' attribute to the currently logged in user's resource URL.
It will set its `data-src' attribute to the currently logged in user's resource URL.
**Example:**
```html
......@@ -28,10 +28,20 @@ will result in :
```html
<sib-conversation data-src="https://your-domain/your-user-uri/3" bind-user></sib-conversation>
```
### Methods available
### Access user profile via Javascript
The logged in user profile is available via the `sib.oidc.user.profile` object.
#### Login
```
document.querySelector('sib-auth').login();
```
#### Logout
```
document.querySelector('sib-auth').logout();
```
#### Get user info
```
document.querySelector('sib-auth').getUser();
```
### Configuration
See [the available properties here](https://github.com/IdentityModel/oidc-client-js/wiki#usermanager)
......@@ -5,18 +5,40 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>SIB-OIDC test</title>
<script src="./node_modules/oidc-client/dist/oidc-client.js"></script>
<script
src="sib-oidc.js"
data-authority="https://test-paris.happy-dev.fr/openid/"
data-client_id="833925"
></script>
<script type="module" src="./index.js"></script>
<style>
sib-test {
display: block;
}
</style>
</head>
<body>
<h1>sib-oidc</h1>
<button id="reset">clear storage and set new URL</button>
<button id="reload">reload</button>
<sib-test>
<button id="login">Se connecter</button>
<button id="logout">Se déconnecter</button>
<button id="stat">Status (console)</button>
<div id="result"></div>
</sib-test>
<sib-auth>
<sib-auth-provider
data-authority="https://test-paris.happy-dev.fr/openid/"
data-client_id="833925"
data-id="paris"
>
</sib-auth-provider>
<sib-auth-provider
data-authority="https://test-paris.happy-dev.fr/openid/"
data-client_id="833925"
data-id="paris2"
>
</sib-auth-provider>
</sib-auth>
<script>
reset.onclick = () => {
window.sessionStorage.clear();
......@@ -26,11 +48,39 @@
reload.onclick = () => location.reload();
</script>
<pre id="output"></pre>
<script>
sib.oidc.getUser().then(user => {
output.textContent = JSON.stringify(user, null, 2);
});
<script type="module">
import { SIBBase } from 'https://unpkg.com/@startinblox/core@0.5';
class TestComponent extends SIBBase {
async connectedCallback() {
stat.onclick = () => console.log(this.getStatus());
login.onclick = () => this.triggerLogin();
logout.onclick = () => this.triggerLogout();
this.update();
}
async triggerLogin() {
await this.login();
}
async triggerLogout() {
await this.logout();
}
async getStatus() {
return this.getUser();
}
update() {
const user = this.getUser();
if (user) {
result.innerHTML = `Bonjour ${user.profile.name} !`;
} else {
result.innerHTML = `Vous n'êtes pas connecté !`;
}
}
}
customElements.define('sib-test', TestComponent);
</script>
</body>
</html>
import SIBAuth from './sib-auth.js';
import SIBAuthProvider from './sib-auth-provider.js';
export {
SIBAuth,
SIBAuthProvider,
};
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -7,19 +7,37 @@
"type": "git",
"url": "git@git.happy-dev.fr:startinblox/framework/sib-oidc.git"
},
"scripts": {
"lint": "eslint --ext .js .",
"test": "npm run lint",
"serve": "live-server --ignore=**/node_modules/** --no-browser"
},
"author": "Startinblox",
"license": "MIT",
"release": {
"branch": "master",
"plugins": [
["@semantic-release/commit-analyzer", {
"preset": "angular",
"releaseRules": [
{"type": "major", "release": "major"},
{"type": "minor", "release": "minor"},
{"type": "/.*/", "release": "patch"}
]
}],
[
"@semantic-release/commit-analyzer",
{
"preset": "angular",
"releaseRules": [
{
"type": "major",
"release": "major"
},
{
"type": "minor",
"release": "minor"
},
{
"type": "/.*/",
"release": "patch"
}
]
}
],
"@semantic-release/release-notes-generator",
"@semantic-release/gitlab",
"@semantic-release/npm"
]
......@@ -29,5 +47,12 @@
},
"dependencies": {
"oidc-client": "^1.6.1"
},
"devDependencies": {
"babel-eslint": "^10.0.1",
"eslint": "^5.12.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-plugin-import": "^2.14.0",
"live-server": "^1.2.1"
}
}
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: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: white;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
align-content: center;
}
sib-auth::before {
content: "Sélectionner un fournisseur d'identité";
font-size: 2em;
width: 100vw;
text-align: center;
}
sib-auth sib-auth-provider {
display: flex;
align-content: middle;
justify-content: center;
align-items: center;
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 sib-auth-provider:hover {
display: flex;
flex
box-shadow: 8px 8px 16px 0 rgba(0, 0, 0, 0.2);
}
import 'https://unpkg.com/oidc-client@1.6';
import { SIBBase, Helpers } from 'https://unpkg.com/@startinblox/core@0.5';
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
*/
constructor() {
super();
this.state = null;
this.user = null;
this.hide();
}
/** @function
* @name connectedCallback
* When called, install method is called
* then we process the current OIDC state
*/
async connectedCallback() {
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
* to provide auth information on all SIB component
*/
install() {
SIBBase.prototype.login = () => this.login();
SIBBase.prototype.logout = () => this.logout();
SIBBase.prototype.getUser = () => this.getUser();
}
/** @function
* @name uninstall
* Remove method and property previously added on SIBBase
*/
// eslint-disable-next-line class-methods-use-this
uninstall() {
SIBBase.prototype.login = null;
SIBBase.prototype.logout = null;
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
* @returns {State} - The state
*/
getState() {
if (!this.state) {
const state = localStorage.getItem('oidc_state');
if (state) {
this.state = JSON.parse(state);
} else {
this.state = {
provider: null,
value: null,
token: null,
previousUri: null,
};
}
}
return this.state;
}
/** @function
* @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, 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,
};
localStorage.setItem('oidc_state', JSON.stringify(state));
this.state = state;
return state;
}
/** @function
* @name clearState
* Reset current state
*/
clearState() {
this.setState();
}
/** @function
* @name processState
* Try to get user, the if a state is set, call the appropriate provider
*/
async processState() {
const { provider } = this.getState();
const providerElement = this.getProvider(provider);
if (providerElement) {
this.callProvider(providerElement, 'processState', this);
}
}
/** @function
* @name dispatchUserInfo
* @param {User} user - User
* Try to replace data-src by user iri on [bind-user] elements
*/
// eslint-disable-next-line class-methods-use-this
async dispatchUserInfo(user) {
const processDOM = async () => {
const id = user.profile.website;
const elements = document.querySelectorAll(`[bind-user]:not([data-src="${id}"])`);
elements.forEach((element) => {
element.setAttribute('data-src', id);
});
};
// check document state and add a hook on DOMContentLoaded if needed
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', processDOM);
} else {
await processDOM();
}
}
/** @function
* @name getUser
* Return User or undefined
* @return {User}
*/
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 setUser
* Set User
* @param {User} user
*/
setUser(user) {
localStorage.setItem('oidc_user', JSON.stringify(user));
this.user = user;
}
/** @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
* Try to logout if favorite provider is set
*/
async logout() {
const favoriteProvider = this.getFavoriteProvider();
if (favoriteProvider) {
this.callProvider(favoriteProvider, 'logout', this);
}
}
/** @function
* @name hide
* Hide provider selector
*/
hide() {
this.initialDisplayStyle = this.style.display;
this.style.display = 'none';
this.removeEventListener('click', this.hide);
}
/** @function
* @name show
* Show provider selector, bind click event
*/
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', SIBAuth);
export default SIBAuth;
import 'https://unpkg.com/oidc-client@1.6';
export default function install () {
const defaultSettings = {
redirect_uri: 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',
};
const settings = Object.assign(
{},
defaultSettings,
document.currentScript.dataset,
);
const manager = new Oidc.UserManager(settings);
const promise = tryToGetUserOrConnect();
const sib = {
oidc: {
getUser: () => promise,
user: null,
},
};
window.sib = sib;
sib.oidc.getUser().then(user => {
sib.oidc.user = user;
});
document.addEventListener('DOMContentLoaded', async () => {
const user = await sib.oidc.getUser();
const id = user.profile.website;
const selector = `[bind-user]:not([data-src="${id}"])`;
const elements = document.querySelectorAll(selector);
for (const element of elements) {
element.setAttribute('data-src', id);
}
});
async function tryToGetUserOrConnect() {
const user = await manager.getUser();
if (user) {
return user;
}
try {
const user = await manager.signinRedirectCallback();
location.href = user.state;
return user;
} catch (e) {
manager.signinRedirect({
state: location.href,
});
throw `Can't get user, redirect to OIDC authority ${settings.authority}`;
}
}
}
install();