From a6fd28fbe6b42309a218cbf5c27c735aea9dbacd Mon Sep 17 00:00:00 2001 From: Maho Murtic Date: Mon, 27 Nov 2017 18:02:29 +0100 Subject: [PATCH 01/15] Split up stuff, replaced rn-uuid with uuid --- package.json | 6 +-- src/Login.js | 104 +++++++++++++++++++++++++++++++++++++ src/TokenStorage.js | 22 ++++++++ src/index.js | 121 +------------------------------------------- 4 files changed, 131 insertions(+), 122 deletions(-) create mode 100644 src/Login.js create mode 100644 src/TokenStorage.js diff --git a/package.json b/package.json index 4ffc316..c21090f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "social", "openid" ], - "author": "Anton Krasovsky", + "author": ["Maho Murtic","Anton Krekovsky"], "license": "MIT", "bugs": { "url": "https://github.com/ak1394/react-native-login/issues" @@ -28,7 +28,7 @@ "homepage": "https://github.com/ak1394/react-native-login#readme", "dependencies": { "base-64": "^0.1.0", - "query-string": "^4.2.3", - "react-native-uuid": "^1.4.8" + "query-string": "^5.0.1", + "uuid": "^3.1.0" } } diff --git a/src/Login.js b/src/Login.js new file mode 100644 index 0000000..c824db1 --- /dev/null +++ b/src/Login.js @@ -0,0 +1,104 @@ +import { Linking } from 'react-native'; +import * as querystring from 'query-string'; +import uuidv4 from 'uuid/v4'; +import { decodeToken } from './util'; + + +export class Login { + state; + conf; + tokenStorage; + + constructor() { + this.state = {}; + this.onOpenURL = this.onOpenURL.bind(this); + Linking.addEventListener('url', this.onOpenURL); + } + + tokens() { + return this.tokenStorage.loadTokens(); + } + + start(conf) { + this.conf = conf; + return new Promise(function(resolve, reject) { + const {url, state} = this.getLoginURL(); + this.state = { + ...this.state, + resolve, + reject, + state, + }; + + Linking.openURL(url); + }.bind(this)); + } + + end() { + return this.tokenStorage.clearTokens(); + } + + onOpenURL(event) { + if(event.url.startsWith(this.conf.appsite_uri)) { + const {state, code} = querystring.parse(querystring.extract(event.url)); + if(this.state.state === state) { + this.retrieveTokens(code); + } + } + } + + retrieveTokens(code) { + const {redirect_uri, client_id} = this.conf; + const url = this.getRealmURL() + '/protocol/openid-connect/token'; + + const headers = new Headers(); + headers.set('Content-Type', 'application/x-www-form-urlencoded'); + + const body = querystring.stringify({ + grant_type: 'authorization_code', + redirect_uri, + client_id, + code + }); + + fetch(url, {method: 'POST', headers, body}).then(response => { + response.json().then(json => { + if(json.error) { + this.state.reject(json); + } else { + this.tokenStorage.saveTokens(json); + this.state.resolve(json); + } + }); + }); + } + + decodeToken(token) { + return decodeToken(token); + } + + getRealmURL() { + const {url, realm} = this.conf; + const slash = url.endsWith('/') ? '' : '/'; + return url + slash + 'realms/' + encodeURIComponent(realm); + } + + getLoginURL() { + const {redirect_uri, client_id, kc_idp_hint} = this.conf; + const response_type = 'code'; + const state = uuidv4(); + const url = this.getRealmURL() + '/protocol/openid-connect/auth?' + querystring.stringify({ + kc_idp_hint, + redirect_uri, + client_id, + response_type, + state, + }); + + return {url, state}; + } + + setTokenStorage(tokenStorage) { + this.tokenStorage = tokenStorage; + } +} diff --git a/src/TokenStorage.js b/src/TokenStorage.js new file mode 100644 index 0000000..776745d --- /dev/null +++ b/src/TokenStorage.js @@ -0,0 +1,22 @@ +import { AsyncStorage } from 'react-native'; + +export class TokenStorage { + key; + constructor(key) { + this.key = key; + } + + saveTokens(tokens) { + return AsyncStorage.setItem(this.key, JSON.stringify(tokens)); + } + + loadTokens() { + return new Promise((resolve, reject) => { + AsyncStorage.getItem(this.key).then(value => resolve(JSON.parse(value))); + }); + } + + clearTokens() { + return AsyncStorage.removeItem(this.key); + } +} diff --git a/src/index.js b/src/index.js index 9f60b65..2c4f950 100644 --- a/src/index.js +++ b/src/index.js @@ -1,122 +1,5 @@ -import { AsyncStorage, Linking } from 'react-native'; -import * as querystring from 'query-string'; -import uuid from 'react-native-uuid'; -import {decodeToken} from './util'; - -class TokenStorage { - constructor(key) { - this.key = key; - } - - saveTokens(tokens) { - return AsyncStorage.setItem(this.key, JSON.stringify(tokens)); - } - - loadTokens() { - return new Promise((resolve, reject) => { - AsyncStorage.getItem(this.key).then(value => resolve(JSON.parse(value))); - }); - } - - clearTokens() { - return AsyncStorage.removeItem(this.key); - } -} - -class Login { - constructor() { - this.state = {}; - this.onOpenURL = this.onOpenURL.bind(this); - Linking.addEventListener('url', this.onOpenURL); - } - - tokens() { - return this.tokenStorage.loadTokens(); - } - - start(conf) { - this.conf = conf; - return new Promise(function(resolve, reject) { - const {url, state} = this.getLoginURL(); - this.state = { - ...this.state, - resolve, - reject, - state, - }; - - Linking.openURL(url); - }.bind(this)); - } - - end() { - return this.tokenStorage.clearTokens(); - } - - onOpenURL(event) { - if(event.url.startsWith(this.conf.appsite_uri)) { - const {state, code} = querystring.parse(querystring.extract(event.url)); - if(this.state.state === state) { - this.retrieveTokens(code); - } - } - } - - retrieveTokens(code) { - const {redirect_uri, client_id} = this.conf; - const url = this.getRealmURL() + '/protocol/openid-connect/token'; - - const headers = new Headers(); - headers.set('Content-Type', 'application/x-www-form-urlencoded'); - - const body = querystring.stringify({ - grant_type: 'authorization_code', - redirect_uri, - client_id, - code - }); - - fetch(url, {method: 'POST', headers, body}).then(response => { - response.json().then(json => { - if(json.error) { - this.state.reject(json); - } else { - this.tokenStorage.saveTokens(json); - this.state.resolve(json); - } - }); - }); - } - - decodeToken(token) { - return decodeToken(token); - } - - getRealmURL() { - const {url, realm} = this.conf; - const slash = url.endsWith('/') ? '' : '/'; - return url + slash + 'realms/' + encodeURIComponent(realm); - } - - getLoginURL() { - const {redirect_uri, client_id, kc_idp_hint} = this.conf; - const response_type = 'code'; - const state = uuid.v4(); - const url = this.getRealmURL() + '/protocol/openid-connect/auth?' + querystring.stringify({ - kc_idp_hint, - redirect_uri, - client_id, - response_type, - state, - }); - - return {url, state}; - } - - setTokenStorage(tokenStorage) { - this.tokenStorage = tokenStorage; - } -} +import { Login } from './Login'; +import { TokenStorage } from './TokenStorage'; const login = new Login(); login.setTokenStorage(new TokenStorage('react-native-token-storage')); From 64b462e39dbbe90c1810e68060e543975f1509b4 Mon Sep 17 00:00:00 2001 From: Maho Murtic Date: Mon, 27 Nov 2017 18:26:20 +0100 Subject: [PATCH 02/15] Fix for openid question --- src/Login.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Login.js b/src/Login.js index c824db1..ed04ba2 100644 --- a/src/Login.js +++ b/src/Login.js @@ -10,7 +10,7 @@ export class Login { tokenStorage; constructor() { - this.state = {}; + this.state = {}; scope, this.onOpenURL = this.onOpenURL.bind(this); Linking.addEventListener('url', this.onOpenURL); } @@ -35,6 +35,7 @@ export class Login { } end() { + // If this is a logout function, you gotta remove it by doing a post or something similar to keycloak. return this.tokenStorage.clearTokens(); } @@ -87,7 +88,9 @@ export class Login { const {redirect_uri, client_id, kc_idp_hint} = this.conf; const response_type = 'code'; const state = uuidv4(); + const scope = 'openid'; const url = this.getRealmURL() + '/protocol/openid-connect/auth?' + querystring.stringify({ + scope, kc_idp_hint, redirect_uri, client_id, From e08d28357ce835a0115a12934519d3551defa91f Mon Sep 17 00:00:00 2001 From: Maho Murtic Date: Tue, 28 Nov 2017 10:54:23 +0100 Subject: [PATCH 03/15] Added eslint, added logout functionality to the server --- .eslintrc | 20 +++++ package.json | 14 ++- src/Login.js | 243 +++++++++++++++++++++++++++++++-------------------- 3 files changed, 181 insertions(+), 96 deletions(-) create mode 100755 .eslintrc diff --git a/.eslintrc b/.eslintrc new file mode 100755 index 0000000..ab66ef7 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,20 @@ +{ + "parser": "babel-eslint", + "extends": "airbnb", + "globals": { + "__DEV__": true + }, + "env": { + "browser": true, + "node": true + }, + "rules": { + "import/no-extraneous-dependencies": 0, + "import/no-unresolved": 0, + "import/prefer-default-export": 0, + "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], + "react/prefer-stateless-function": 0, + "max-len": ["error",{ "code":120 }], + }, + "plugins": ["react"] +} diff --git a/package.json b/package.json index c21090f..c22bbd0 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,10 @@ "social", "openid" ], - "author": ["Maho Murtic","Anton Krekovsky"], + "author": [ + "Maho Murtic", + "Anton Krekovsky" + ], "license": "MIT", "bugs": { "url": "https://github.com/ak1394/react-native-login/issues" @@ -28,7 +31,16 @@ "homepage": "https://github.com/ak1394/react-native-login#readme", "dependencies": { "base-64": "^0.1.0", + "eslint-plugin-react": "^7.5.1", "query-string": "^5.0.1", "uuid": "^3.1.0" + }, + "devDependencies": { + "babel-eslint": "^8.0.2", + "eslint": "^4.12.0", + "eslint-config-airbnb": "^16.1.0", + "eslint-plugin-import": "^2.8.0", + "eslint-plugin-jsx-a11y": "^6.0.2", + "eslint-plugin-react": "^7.5.1" } } diff --git a/src/Login.js b/src/Login.js index ed04ba2..54dd3ef 100644 --- a/src/Login.js +++ b/src/Login.js @@ -1,107 +1,160 @@ -import { Linking } from 'react-native'; +import { + Linking, +} from 'react-native'; import * as querystring from 'query-string'; import uuidv4 from 'uuid/v4'; -import { decodeToken } from './util'; +import { + decodeToken, +} from './util'; export class Login { - state; - conf; - tokenStorage; - - constructor() { - this.state = {}; scope, - this.onOpenURL = this.onOpenURL.bind(this); - Linking.addEventListener('url', this.onOpenURL); - } - - tokens() { - return this.tokenStorage.loadTokens(); - } - - start(conf) { - this.conf = conf; - return new Promise(function(resolve, reject) { - const {url, state} = this.getLoginURL(); - this.state = { - ...this.state, - resolve, - reject, - state, - }; + state; + conf; + tokenStorage; + + constructor() { + this.state = {}; + this.onOpenURL = this.onOpenURL.bind(this); + Linking.addEventListener('url', this.onOpenURL); + } - Linking.openURL(url); - }.bind(this)); - } + tokens() { + return this.tokenStorage.loadTokens(); + } - end() { - // If this is a logout function, you gotta remove it by doing a post or something similar to keycloak. - return this.tokenStorage.clearTokens(); - } + start(conf) { + this.conf = conf; + return new Promise(((resolve, reject) => { + const { + url, + state, + } = this.getLoginURL(); + this.state = { + ...this.state, + resolve, + reject, + state, + }; + + Linking.openURL(url); + })); + } - onOpenURL(event) { - if(event.url.startsWith(this.conf.appsite_uri)) { - const {state, code} = querystring.parse(querystring.extract(event.url)); - if(this.state.state === state) { - this.retrieveTokens(code); - } + end() { + const { + client_id, + redirect_uri, + } = this.conf; + // If this is a logout function, you gotta remove it by doing a post or something similar to keycloak. + const headers = new Headers(); + headers.set('Content-Type', 'application/x-www-form-urlencoded'); + this.tokens().then((savedTokens) => { + const body = querystring.stringify({ + client_id, + refresh_token: savedTokens.refresh_token, + }); + + const url = `${this.getRealmURL()}/protocol/openid-connect/logout`; + fetch(url, { + method: 'POST', + headers, + body, + }).then((response) => { + if (response.ok) { + this.tokenStorage.clearTokens(); + return true; + } + return false; + }); + }); } - } - - retrieveTokens(code) { - const {redirect_uri, client_id} = this.conf; - const url = this.getRealmURL() + '/protocol/openid-connect/token'; - - const headers = new Headers(); - headers.set('Content-Type', 'application/x-www-form-urlencoded'); - - const body = querystring.stringify({ - grant_type: 'authorization_code', - redirect_uri, - client_id, - code - }); - - fetch(url, {method: 'POST', headers, body}).then(response => { - response.json().then(json => { - if(json.error) { - this.state.reject(json); - } else { - this.tokenStorage.saveTokens(json); - this.state.resolve(json); + + onOpenURL(event) { + if (event.url.startsWith(this.conf.appsite_uri)) { + const { + state, + code, + } = querystring.parse(querystring.extract(event.url)); + if (this.state.state === state) { + this.retrieveTokens(code); } + } + } + + retrieveTokens(code) { + const { + redirect_uri, + client_id, + } = this.conf; + const url = `${this.getRealmURL()}/protocol/openid-connect/token`; + + const headers = new Headers(); + headers.set('Content-Type', 'application/x-www-form-urlencoded'); + + const body = querystring.stringify({ + grant_type: 'authorization_code', + redirect_uri, + client_id, + code, + }); + + fetch(url, { + method: 'POST', + headers, + body, + }).then((response) => { + response.json().then((json) => { + // console.log(json); + if (json.error) { + this.state.reject(json); + } else { + // console.log(json); + this.tokenStorage.saveTokens(json); + this.state.resolve(json); + } + }); }); - }); - } - - decodeToken(token) { - return decodeToken(token); - } - - getRealmURL() { - const {url, realm} = this.conf; - const slash = url.endsWith('/') ? '' : '/'; - return url + slash + 'realms/' + encodeURIComponent(realm); - } - - getLoginURL() { - const {redirect_uri, client_id, kc_idp_hint} = this.conf; - const response_type = 'code'; - const state = uuidv4(); - const scope = 'openid'; - const url = this.getRealmURL() + '/protocol/openid-connect/auth?' + querystring.stringify({ - scope, - kc_idp_hint, - redirect_uri, - client_id, - response_type, - state, - }); - - return {url, state}; - } - - setTokenStorage(tokenStorage) { - this.tokenStorage = tokenStorage; - } + } + + decodeToken(token) { + return decodeToken(token); + } + + getRealmURL() { + const { + url, + realm, + } = this.conf; + const slash = url.endsWith('/') ? '' : '/'; + return `${url + slash}realms/${encodeURIComponent(realm)}`; + } + + getLoginURL() { + const { + redirect_uri, + client_id, + kc_idp_hint, + } = this.conf; + const response_type = 'code'; + const state = uuidv4(); + const scope = 'openid'; + const url = `${this.getRealmURL()}/protocol/openid-connect/auth?${querystring.stringify({ + scope, + kc_idp_hint, + redirect_uri, + client_id, + response_type, + state, + })}`; + + return { + url, + state, + }; + } + + setTokenStorage(tokenStorage) { + this.tokenStorage = tokenStorage; + } } From 4db8b37b5e0963528770781a9090b1f607d37c08 Mon Sep 17 00:00:00 2001 From: Maho Murtic Date: Mon, 4 Dec 2017 18:10:10 +0100 Subject: [PATCH 04/15] Modified a bit --- src/Login.js | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Login.js b/src/Login.js index 54dd3ef..7083776 100644 --- a/src/Login.js +++ b/src/Login.js @@ -1,10 +1,8 @@ -import { - Linking, -} from 'react-native'; +import { Linking } from 'react-native'; import * as querystring from 'query-string'; import uuidv4 from 'uuid/v4'; import { - decodeToken, + decodeToken as decodeTokenImported, } from './util'; @@ -41,31 +39,35 @@ export class Login { })); } + setConf(conf) { + this.conf = conf; + } + end() { const { client_id, - redirect_uri, } = this.conf; // If this is a logout function, you gotta remove it by doing a post or something similar to keycloak. const headers = new Headers(); headers.set('Content-Type', 'application/x-www-form-urlencoded'); - this.tokens().then((savedTokens) => { + return this.tokens().then((savedTokens) => { + if (!savedTokens) { + throw new Error('No saved tokens'); + } const body = querystring.stringify({ client_id, refresh_token: savedTokens.refresh_token, }); const url = `${this.getRealmURL()}/protocol/openid-connect/logout`; - fetch(url, { + return fetch(url, { method: 'POST', headers, body, }).then((response) => { if (response.ok) { this.tokenStorage.clearTokens(); - return true; } - return false; }); }); } @@ -105,11 +107,9 @@ export class Login { body, }).then((response) => { response.json().then((json) => { - // console.log(json); if (json.error) { this.state.reject(json); } else { - // console.log(json); this.tokenStorage.saveTokens(json); this.state.resolve(json); } @@ -117,8 +117,25 @@ export class Login { }); } + retrieveUserInfo() { + return this.tokens().then((savedTokens) => { + if (!savedTokens) { + throw new Error('no tokens found'); + } + const headers = new Headers(); + headers.set('Content-Type', 'application/x-www-form-urlencoded'); + headers.set('Authorization', `Bearer ${savedTokens.access_token}`); + + const url = `${this.getRealmURL()}/protocol/openid-connect/userinfo`; + return fetch(url, { + method: 'GET', + headers, + }).then(response => response.json()).catch(() => {}); + }); + } + decodeToken(token) { - return decodeToken(token); + return decodeTokenImported(token); } getRealmURL() { From d3ad257ffe0958a82b91a5805aada010e4021779 Mon Sep 17 00:00:00 2001 From: Maho Murtic Date: Tue, 5 Dec 2017 13:40:24 +0100 Subject: [PATCH 05/15] Changes to names --- src/Login.js | 60 ++++++++++++++++++---------------------------------- 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/src/Login.js b/src/Login.js index 7083776..e7d8ee8 100644 --- a/src/Login.js +++ b/src/Login.js @@ -1,55 +1,50 @@ import { Linking } from 'react-native'; import * as querystring from 'query-string'; import uuidv4 from 'uuid/v4'; -import { - decodeToken as decodeTokenImported, -} from './util'; +import { decodeToken } from './util'; export class Login { state; conf; tokenStorage; + headers; + decodeToken; constructor() { this.state = {}; this.onOpenURL = this.onOpenURL.bind(this); Linking.addEventListener('url', this.onOpenURL); + this.headers = new Headers(); + this.headers.set('Content-Type', 'application/x-www-form-urlencoded'); + this.decodeToken = decodeToken; } - tokens() { + getTokens() { return this.tokenStorage.loadTokens(); } - start(conf) { - this.conf = conf; + startLoginProcess(conf) { + this.setConf(conf); return new Promise(((resolve, reject) => { - const { - url, - state, - } = this.getLoginURL(); + const { url, state } = this.getLoginURL(); this.state = { ...this.state, resolve, reject, state, }; - Linking.openURL(url); })); } setConf(conf) { - this.conf = conf; + this.conf = (conf) || undefined; } - end() { - const { - client_id, - } = this.conf; - // If this is a logout function, you gotta remove it by doing a post or something similar to keycloak. - const headers = new Headers(); - headers.set('Content-Type', 'application/x-www-form-urlencoded'); + logoutKc() { + const { client_id } = this.conf; + return this.tokens().then((savedTokens) => { if (!savedTokens) { throw new Error('No saved tokens'); @@ -62,7 +57,7 @@ export class Login { const url = `${this.getRealmURL()}/protocol/openid-connect/logout`; return fetch(url, { method: 'POST', - headers, + headers: this.headers, body, }).then((response) => { if (response.ok) { @@ -91,9 +86,6 @@ export class Login { } = this.conf; const url = `${this.getRealmURL()}/protocol/openid-connect/token`; - const headers = new Headers(); - headers.set('Content-Type', 'application/x-www-form-urlencoded'); - const body = querystring.stringify({ grant_type: 'authorization_code', redirect_uri, @@ -103,7 +95,7 @@ export class Login { fetch(url, { method: 'POST', - headers, + headers: this.headers, body, }).then((response) => { response.json().then((json) => { @@ -122,22 +114,16 @@ export class Login { if (!savedTokens) { throw new Error('no tokens found'); } - const headers = new Headers(); - headers.set('Content-Type', 'application/x-www-form-urlencoded'); - headers.set('Authorization', `Bearer ${savedTokens.access_token}`); + this.headers.set('Authorization', `Bearer ${savedTokens.access_token}`); const url = `${this.getRealmURL()}/protocol/openid-connect/userinfo`; return fetch(url, { method: 'GET', - headers, + headers: this.headers, }).then(response => response.json()).catch(() => {}); }); } - decodeToken(token) { - return decodeTokenImported(token); - } - getRealmURL() { const { url, @@ -148,12 +134,8 @@ export class Login { } getLoginURL() { - const { - redirect_uri, - client_id, - kc_idp_hint, - } = this.conf; - const response_type = 'code'; + const { redirect_uri, client_id, kc_idp_hint } = this.conf; + const responseType = 'code'; const state = uuidv4(); const scope = 'openid'; const url = `${this.getRealmURL()}/protocol/openid-connect/auth?${querystring.stringify({ @@ -161,7 +143,7 @@ export class Login { kc_idp_hint, redirect_uri, client_id, - response_type, + response_type: responseType, state, })}`; From fe65067f340a4dca2cd6f54a78519321bf2c6069 Mon Sep 17 00:00:00 2001 From: Maho Murtic Date: Fri, 15 Dec 2017 11:23:26 +0100 Subject: [PATCH 06/15] Promise -> async-await + refactor --- src/Login.js | 163 ++++++++++++++++++++++---------------- src/{util.js => Utils.js} | 10 +-- 2 files changed, 99 insertions(+), 74 deletions(-) rename src/{util.js => Utils.js} (75%) diff --git a/src/Login.js b/src/Login.js index e7d8ee8..c786c70 100644 --- a/src/Login.js +++ b/src/Login.js @@ -1,23 +1,28 @@ import { Linking } from 'react-native'; import * as querystring from 'query-string'; import uuidv4 from 'uuid/v4'; -import { decodeToken } from './util'; - export class Login { state; conf; tokenStorage; headers; - decodeToken; constructor() { this.state = {}; this.onOpenURL = this.onOpenURL.bind(this); Linking.addEventListener('url', this.onOpenURL); - this.headers = new Headers(); - this.headers.set('Content-Type', 'application/x-www-form-urlencoded'); - this.decodeToken = decodeToken; + + this.props = { + requestOptions: { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'GET', + body: undefined, + }, + url: '', + }; } getTokens() { @@ -39,32 +44,29 @@ export class Login { } setConf(conf) { - this.conf = (conf) || undefined; + if (conf) { + this.conf = conf; + } } - logoutKc() { + async logoutKc() { const { client_id } = this.conf; + const savedTokens = await this.getTokens(); + if (!savedTokens) { + return undefined; + } - return this.tokens().then((savedTokens) => { - if (!savedTokens) { - throw new Error('No saved tokens'); - } - const body = querystring.stringify({ - client_id, - refresh_token: savedTokens.refresh_token, - }); - - const url = `${this.getRealmURL()}/protocol/openid-connect/logout`; - return fetch(url, { - method: 'POST', - headers: this.headers, - body, - }).then((response) => { - if (response.ok) { - this.tokenStorage.clearTokens(); - } - }); - }); + this.props.url = `${this.getRealmURL()}/protocol/openid-connect/logout`; + + this.setRequestOptions('POST', querystring.stringify({ client_id, refresh_token: savedTokens.refresh_token })); + + const fullResponse = await fetch(this.props.url, this.props.requestOptions); + + if (fullResponse.ok) { + this.tokenStorage.clearTokens(); + return true; + } + return false; } onOpenURL(event) { @@ -79,56 +81,67 @@ export class Login { } } - retrieveTokens(code) { - const { - redirect_uri, - client_id, - } = this.conf; - const url = `${this.getRealmURL()}/protocol/openid-connect/token`; - const body = querystring.stringify({ - grant_type: 'authorization_code', - redirect_uri, - client_id, - code, - }); + async retrieveTokens(code) { + const { redirect_uri, client_id } = this.conf; + this.props.url = `${this.getRealmURL()}/protocol/openid-connect/token`; - fetch(url, { - method: 'POST', - headers: this.headers, - body, - }).then((response) => { - response.json().then((json) => { - if (json.error) { - this.state.reject(json); - } else { - this.tokenStorage.saveTokens(json); - this.state.resolve(json); - } - }); - }); + this.setRequestOptions('POST', querystring.stringify({ + grant_type: 'authorization_code', redirect_uri, client_id, code, + })); + + const fullResponse = await fetch(this.props.url, this.props.requestOptions); + const jsonResponse = await fullResponse.json(); + if (fullResponse.ok) { + this.tokenStorage.saveTokens(jsonResponse); + this.state.resolve(jsonResponse); + } else { + this.state.reject(jsonResponse); + } } - retrieveUserInfo() { - return this.tokens().then((savedTokens) => { - if (!savedTokens) { - throw new Error('no tokens found'); + async retrieveUserInfo() { + const savedTokens = await this.getTokens(); + if (savedTokens) { + this.props.url = `${this.getRealmURL()}/protocol/openid-connect/userinfo`; + + this.setHeader('Authorization', `Bearer ${savedTokens.access_token}`); + this.setRequestOptions('GET'); + + const fullResponse = await fetch(this.props.url, this.props.requestOptions); + if (fullResponse.ok) { + return fullResponse.json(); } - this.headers.set('Authorization', `Bearer ${savedTokens.access_token}`); + } + return undefined; + } - const url = `${this.getRealmURL()}/protocol/openid-connect/userinfo`; - return fetch(url, { - method: 'GET', - headers: this.headers, - }).then(response => response.json()).catch(() => {}); - }); + async refreshToken() { + const savedTokens = await this.getTokens(); + if (!savedTokens) { + return undefined; + } + + const { client_id } = this.conf; + this.props.url = `${this.getRealmURL()}/protocol/openid-connect/token`; + + this.setRequestOptions('POST', querystring.stringify({ + grant_type: 'refresh_token', + refresh_token: savedTokens.refresh_token, + client_id: encodeURIComponent(client_id), + })); + + const fullResponse = await fetch(this.props.url, this.props.requestOptions); + if (fullResponse.ok) { + const jsonResponse = await fullResponse.json(); + this.tokenStorage.saveTokens(jsonResponse); + return jsonResponse; + } + return undefined; } getRealmURL() { - const { - url, - realm, - } = this.conf; + const { url, realm } = this.conf; const slash = url.endsWith('/') ? '' : '/'; return `${url + slash}realms/${encodeURIComponent(realm)}`; } @@ -156,4 +169,16 @@ export class Login { setTokenStorage(tokenStorage) { this.tokenStorage = tokenStorage; } + + setRequestOptions(method, body) { + this.props.requestOptions = { + ...this.props.requestOptions, + method, + body, + }; + } + + setHeader(key, value) { + this.props.requestOptions.headers[key] = value; + } } diff --git a/src/util.js b/src/Utils.js similarity index 75% rename from src/util.js rename to src/Utils.js index 7e125e8..a717869 100644 --- a/src/util.js +++ b/src/Utils.js @@ -7,18 +7,18 @@ export function decodeToken(token) { str = str.replace('/_/g', '/'); switch (str.length % 4) { case 0: - break; + break; case 2: str += '=='; - break; + break; case 3: str += '='; - break; + break; default: - throw 'Invalid token'; + throw new Error('Invalid token'); } - str = (str + '===').slice(0, str.length + (str.length % 4)); + str = (`${str}===`).slice(0, str.length + (str.length % 4)); str = str.replace(/-/g, '+').replace(/_/g, '/'); str = decodeURIComponent(escape(base64.decode(str))); From b27533aba0fce5bac1d51d132250e3489db14b93 Mon Sep 17 00:00:00 2001 From: mahomahoxd Date: Fri, 15 Dec 2017 11:43:28 +0100 Subject: [PATCH 07/15] Removed unused header variable, added accept json only in header --- src/Login.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Login.js b/src/Login.js index c786c70..23372aa 100644 --- a/src/Login.js +++ b/src/Login.js @@ -6,7 +6,6 @@ export class Login { state; conf; tokenStorage; - headers; constructor() { this.state = {}; @@ -16,6 +15,7 @@ export class Login { this.props = { requestOptions: { headers: { + Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', }, method: 'GET', From c93657e664dae1157550d66dd61f1d43ec25a254 Mon Sep 17 00:00:00 2001 From: mahomahoxd Date: Thu, 21 Dec 2017 16:14:11 +0100 Subject: [PATCH 08/15] Updated readme, eslintrc, a few code issues --- .eslintrc | 13 +++++-- README.md | 94 ++++++++++++++++++++------------------------- package.json | 10 +++-- src/Login.js | 30 +++++++++------ src/TokenStorage.js | 2 +- src/index.js | 2 +- 6 files changed, 78 insertions(+), 73 deletions(-) diff --git a/.eslintrc b/.eslintrc index ab66ef7..32a503e 100755 --- a/.eslintrc +++ b/.eslintrc @@ -2,6 +2,7 @@ "parser": "babel-eslint", "extends": "airbnb", "globals": { + "fetch": true, "__DEV__": true }, "env": { @@ -12,9 +13,15 @@ "import/no-extraneous-dependencies": 0, "import/no-unresolved": 0, "import/prefer-default-export": 0, - "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], "react/prefer-stateless-function": 0, - "max-len": ["error",{ "code":120 }], + "max-len": ["error",{ "code":120 }] }, - "plugins": ["react"] + "settings": { + "import/resolver": { + "node": { + "extensions": [".js", ".android.js", ".ios.js"] + } + } + }, + "plugins": ["react", "react-native"] } diff --git a/README.md b/README.md index d841351..e2373d9 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,58 @@ # React Native Login - -React Native Login is a module for [React Native](https://facebook.github.io/react-native/) for implementing lightweight universal authentication using [Keycloak](http://keycloak.org) - - -See [Simple social login for React Native apps](https://medium.com/@ak1394/simple-social-login-for-react-native-apps-71279bf80ffc) for details. +This is a fork of ak1394's React-Native-Login module. It's a version that I'm planning to maintenance more than it's been with ak1394. ## Documentation -- [Install](https://github.com/ak1394/react-native-login#install) -- [Usage](https://github.com/ak1394/react-native-login#usage) -- [Example](https://github.com/ak1394/react-native-login#example) -- [License](https://github.com/ak1394/react-native-login#license) +- [Install](https://github.com/mahomahoxd/react-native-login#install) +- [Usage](https://github.com/mahomahoxd/react-native-login#usage) ## Install ```shell -npm install --save react-native-login +npm i --save react-native-login-keycloak ``` ## Usage ### App configuration -Please configure [Linking](https://facebook.github.io/react-native/docs/linking.html) module, including steps for handling Universal links. +Please configure [Linking](https://facebook.github.io/react-native/docs/linking.html) module, including steps for handling Universal links (This might get changed due to not being able to close the tab on leave, ending up with a lot of tabs in the browser). -Also, add applinks: entry to Associated Domains Capability of your app. +Also, add the applinks: entry to the Associated Domains Capability of your app. ### Imports ```js -import Login from 'react-native-login'; +import Login from 'react-native-login-keycloak'; ``` -### Checking if user is logged in +### Checking if tokens are saved on the device ```js -Login.tokens().then(tokens => { - console.log(tokens); -}); +const gatheredTokens = await Login.getTokens(); +console.log(gatheredTokens); // Prints: // // { access_token: '...', refresh_token: '...', id_token: '...', ...} +// OR +// undefined ``` ### Login - ```js const config = { url: 'https:///auth', realm: '', - client_id: '', - redirect_uri: 'https:///success.html', - appsite_uri: 'https:///app.html', - kc_idp_hint: 'facebook', + clientId: '', + redirectUri: 'https:///success.html', + appsiteUri: 'https:///app.html', + kcIdpHint: 'facebook', // *optional* }; -Login.start(config).then(tokens => { +Login.startLoginProcess(config).then(tokens => { console.log(tokens); }); @@ -67,44 +61,40 @@ Login.start(config).then(tokens => { // { access_token: '...', refresh_token: '...', id_token: '...', ...} ``` -Initiates login flow. Upon successfull completion, saves and returns a set of tokens. - -### Logout +Logging in by the startLoginProcess function will save it in the AsyncStorage, whereas after its been successful, getTokens will get the most recent tokens that are saved and you can then use it to authenticate against a backend. +### Refreshing the token ```js -Login.end(); +const refreshedTokens = await Login.refreshToken(); +console.log(refreshTokens); +// Prints: +// +// { access_token: '...', refresh_token: '...', id_token: '...', ...} +// OR +// undefined ``` -Removes stored tokens. Subsequent calls to Login.tokens() will return null. -## Example -Please see the example app [react-native-login-example](https://github.com/ak1394/react-native-login-example) +### Retrieving logged in user info +```js +const loggedInUser = await Login.retrieveUserInfo(); +console.log(loggedInUser); -## License +// Prints: +// +// { sub: '...',name: '... ',preferred_username: '...',given_name: '...' } -The MIT License (MIT) -===================== +// OR +// undefined +``` -Copyright © `2016` `Anton Krasovsky` -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the “Software”), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: +### Logout -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +```js +Login.logoutKc(); +``` +Removes stored tokens. Will also do a Keycloak call to log the user out. Returns true on logout, else false. Subsequent calls to Login.tokens() will return null. -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. +If you got any improvements feel free to make a pull request or suggestion. diff --git a/package.json b/package.json index c22bbd0..0d68dbc 100644 --- a/package.json +++ b/package.json @@ -33,14 +33,16 @@ "base-64": "^0.1.0", "eslint-plugin-react": "^7.5.1", "query-string": "^5.0.1", + "react-native": "^0.51.0", "uuid": "^3.1.0" }, "devDependencies": { - "babel-eslint": "^8.0.2", - "eslint": "^4.12.0", + "babel-eslint": "^8.0.3", + "eslint": "^4.13.1", "eslint-config-airbnb": "^16.1.0", "eslint-plugin-import": "^2.8.0", - "eslint-plugin-jsx-a11y": "^6.0.2", - "eslint-plugin-react": "^7.5.1" + "eslint-plugin-jsx-a11y": "^6.0.3", + "eslint-plugin-react": "^7.5.1", + "eslint-plugin-react-native": "^3.2.0" } } diff --git a/src/Login.js b/src/Login.js index 23372aa..1d78dbe 100644 --- a/src/Login.js +++ b/src/Login.js @@ -50,7 +50,7 @@ export class Login { } async logoutKc() { - const { client_id } = this.conf; + const { clientId } = this.conf; const savedTokens = await this.getTokens(); if (!savedTokens) { return undefined; @@ -58,7 +58,10 @@ export class Login { this.props.url = `${this.getRealmURL()}/protocol/openid-connect/logout`; - this.setRequestOptions('POST', querystring.stringify({ client_id, refresh_token: savedTokens.refresh_token })); + this.setRequestOptions( + 'POST', + querystring.stringify({ client_id: clientId, refresh_token: savedTokens.refresh_token }), + ); const fullResponse = await fetch(this.props.url, this.props.requestOptions); @@ -83,12 +86,15 @@ export class Login { async retrieveTokens(code) { - const { redirect_uri, client_id } = this.conf; + const { redirectUri, clientId } = this.conf; this.props.url = `${this.getRealmURL()}/protocol/openid-connect/token`; - this.setRequestOptions('POST', querystring.stringify({ - grant_type: 'authorization_code', redirect_uri, client_id, code, - })); + this.setRequestOptions( + 'POST', + querystring.stringify({ + grant_type: 'authorization_code', redirect_uri: redirectUri, client_id: clientId, code, + }), + ); const fullResponse = await fetch(this.props.url, this.props.requestOptions); const jsonResponse = await fullResponse.json(); @@ -122,13 +128,13 @@ export class Login { return undefined; } - const { client_id } = this.conf; + const { clientId } = this.conf; this.props.url = `${this.getRealmURL()}/protocol/openid-connect/token`; this.setRequestOptions('POST', querystring.stringify({ grant_type: 'refresh_token', refresh_token: savedTokens.refresh_token, - client_id: encodeURIComponent(client_id), + client_id: encodeURIComponent(clientId), })); const fullResponse = await fetch(this.props.url, this.props.requestOptions); @@ -147,15 +153,15 @@ export class Login { } getLoginURL() { - const { redirect_uri, client_id, kc_idp_hint } = this.conf; + const { redirectUri, clientId, kcIdpHint } = this.conf; const responseType = 'code'; const state = uuidv4(); const scope = 'openid'; const url = `${this.getRealmURL()}/protocol/openid-connect/auth?${querystring.stringify({ scope, - kc_idp_hint, - redirect_uri, - client_id, + kc_idp_hint: kcIdpHint, + redirect_uri: redirectUri, + client_id: clientId, response_type: responseType, state, })}`; diff --git a/src/TokenStorage.js b/src/TokenStorage.js index 776745d..198907f 100644 --- a/src/TokenStorage.js +++ b/src/TokenStorage.js @@ -11,7 +11,7 @@ export class TokenStorage { } loadTokens() { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { AsyncStorage.getItem(this.key).then(value => resolve(JSON.parse(value))); }); } diff --git a/src/index.js b/src/index.js index 2c4f950..3311afd 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,6 @@ import { Login } from './Login'; import { TokenStorage } from './TokenStorage'; const login = new Login(); -login.setTokenStorage(new TokenStorage('react-native-token-storage')); +login.setTokenStorage(new TokenStorage('react-native-keycloak-tokens')); export default login; From e7036807d17050ab5c3c868e24c095149e68a4c3 Mon Sep 17 00:00:00 2001 From: mahomahoxd Date: Wed, 27 Dec 2017 22:20:21 +0100 Subject: [PATCH 09/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2373d9..ef81277 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# React Native Login +# react-native-login-keycloak This is a fork of ak1394's React-Native-Login module. It's a version that I'm planning to maintenance more than it's been with ak1394. ## Documentation From 0d96593a554ce7da6ab092082d750d1ac1756397 Mon Sep 17 00:00:00 2001 From: mahomahoxd Date: Wed, 27 Dec 2017 22:22:41 +0100 Subject: [PATCH 10/15] Updated package.json --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 0d68dbc..8e1c016 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { - "name": "react-native-login", - "version": "0.0.1-alpha.2", - "description": "React Native module for lightweight universal authentication using Keycloak", + "name": "react-native-login-keycloak", + "version": "1.0.0", + "description": "React Native module for authentication between a client and the keycloak server.", "main": "src/index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", - "url": "git+https://github.com/ak1394/react-native-login.git" + "url": "git+https://github.com/mahomahoxd/react-native-login-keycloak.git" }, "keywords": [ "auth", @@ -26,9 +26,9 @@ ], "license": "MIT", "bugs": { - "url": "https://github.com/ak1394/react-native-login/issues" + "url": "https://github.com/mahomahoxd/react-native-login-keycloak/issues" }, - "homepage": "https://github.com/ak1394/react-native-login#readme", + "homepage": "https://github.com/mahomahoxd/react-native-login-keycloak#readme", "dependencies": { "base-64": "^0.1.0", "eslint-plugin-react": "^7.5.1", From 60fb0bf3c826773b261a06376055a26aac95be15 Mon Sep 17 00:00:00 2001 From: mahomahoxd Date: Wed, 27 Dec 2017 22:24:36 +0100 Subject: [PATCH 11/15] Merge branch 'master' of https://github.com/mahomahoxd/react-native-login --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2373d9..ef81277 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# React Native Login +# react-native-login-keycloak This is a fork of ak1394's React-Native-Login module. It's a version that I'm planning to maintenance more than it's been with ak1394. ## Documentation From 9374fff95a953fbf0980b710e1a0f99979a6cb83 Mon Sep 17 00:00:00 2001 From: mahomahoxd Date: Tue, 2 Jan 2018 11:08:00 +0100 Subject: [PATCH 12/15] Adjusted a small piece so it works again --- src/Login.js | 2 +- src/TokenStorage.js | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Login.js b/src/Login.js index 1d78dbe..a58868a 100644 --- a/src/Login.js +++ b/src/Login.js @@ -73,7 +73,7 @@ export class Login { } onOpenURL(event) { - if (event.url.startsWith(this.conf.appsite_uri)) { + if (event.url.startsWith(this.conf.appsiteUri)) { const { state, code, diff --git a/src/TokenStorage.js b/src/TokenStorage.js index 198907f..a043e79 100644 --- a/src/TokenStorage.js +++ b/src/TokenStorage.js @@ -10,10 +10,9 @@ export class TokenStorage { return AsyncStorage.setItem(this.key, JSON.stringify(tokens)); } - loadTokens() { - return new Promise((resolve) => { - AsyncStorage.getItem(this.key).then(value => resolve(JSON.parse(value))); - }); + async loadTokens() { + const tokens = await AsyncStorage.getItem(this.key); + return (tokens) ? JSON.parse(tokens) : undefined; } clearTokens() { From a36c3134dec977a6da2372f9d5e4ad06f6e4bfc3 Mon Sep 17 00:00:00 2001 From: mahomahoxd Date: Wed, 3 Jan 2018 15:56:13 +0100 Subject: [PATCH 13/15] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e1c016..e2f7df9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-login-keycloak", - "version": "1.0.0", + "version": "1.0.1", "description": "React Native module for authentication between a client and the keycloak server.", "main": "src/index.js", "scripts": { From d79c17f6e5d048c0535e216ebcbbbe55cabf092a Mon Sep 17 00:00:00 2001 From: Maho Date: Mon, 15 Jan 2018 14:50:13 +0100 Subject: [PATCH 14/15] react-native was not necessary apparently --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index e2f7df9..c4bcf93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-login-keycloak", - "version": "1.0.1", + "version": "1.0.2", "description": "React Native module for authentication between a client and the keycloak server.", "main": "src/index.js", "scripts": { @@ -33,7 +33,6 @@ "base-64": "^0.1.0", "eslint-plugin-react": "^7.5.1", "query-string": "^5.0.1", - "react-native": "^0.51.0", "uuid": "^3.1.0" }, "devDependencies": { From 9fa133ddecf8da9aa9b9d0bf54ba83e27f90c01a Mon Sep 17 00:00:00 2001 From: sharavana006 Date: Fri, 9 Nov 2018 14:51:57 +0530 Subject: [PATCH 15/15] Added optional parameters --- src/Login.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Login.js b/src/Login.js index a58868a..f5db52c 100644 --- a/src/Login.js +++ b/src/Login.js @@ -153,7 +153,7 @@ export class Login { } getLoginURL() { - const { redirectUri, clientId, kcIdpHint } = this.conf; + const { redirectUri, clientId, kcIdpHint,options } = this.conf; const responseType = 'code'; const state = uuidv4(); const scope = 'openid'; @@ -163,6 +163,7 @@ export class Login { redirect_uri: redirectUri, client_id: clientId, response_type: responseType, + options:options, state, })}`;