mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
#48: add support for prompt and login_hint oauth params
This commit is contained in:
@@ -450,17 +450,23 @@ class PanelTransition extends Component {
|
|||||||
|
|
||||||
export default connect((state) => {
|
export default connect((state) => {
|
||||||
const {login} = state.auth;
|
const {login} = state.auth;
|
||||||
const user = {
|
let user = {
|
||||||
...state.user,
|
...state.user
|
||||||
isGuest: true,
|
|
||||||
email: '',
|
|
||||||
username: ''
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (/[@.]/.test(login)) {
|
if (login) {
|
||||||
user.email = login;
|
user = {
|
||||||
} else {
|
...user,
|
||||||
user.username = login;
|
isGuest: true,
|
||||||
|
email: '',
|
||||||
|
username: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
if (/[@.]/.test(login)) {
|
||||||
|
user.email = login;
|
||||||
|
} else {
|
||||||
|
user.username = login;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export function clearErrors() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { logout, updateUser } from 'components/user/actions';
|
export { logout, updateUser } from 'components/user/actions';
|
||||||
|
export { authenticate } from 'components/accounts/actions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} oauthData
|
* @param {object} oauthData
|
||||||
@@ -153,6 +154,13 @@ export { logout, updateUser } from 'components/user/actions';
|
|||||||
* @param {string} oauthData.responseType
|
* @param {string} oauthData.responseType
|
||||||
* @param {string} oauthData.description
|
* @param {string} oauthData.description
|
||||||
* @param {string} oauthData.scope
|
* @param {string} oauthData.scope
|
||||||
|
* @param {string} [oauthData.prompt='none'] - comma-separated list of values to adjust auth flow
|
||||||
|
* Posible values:
|
||||||
|
* * none - default behaviour
|
||||||
|
* * consent - forcibly prompt user for rules acceptance
|
||||||
|
* * select_account - force account choosage, even if user has only one
|
||||||
|
* @param {string} oauthData.loginHint - allows to choose the account, which will be used for auth
|
||||||
|
* The possible values: account id, email, username
|
||||||
* @param {string} oauthData.state
|
* @param {string} oauthData.state
|
||||||
*
|
*
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
@@ -163,8 +171,17 @@ export function oAuthValidate(oauthData) {
|
|||||||
return wrapInLoader((dispatch) =>
|
return wrapInLoader((dispatch) =>
|
||||||
oauth.validate(oauthData)
|
oauth.validate(oauthData)
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
|
let prompt = (oauthData.prompt || 'none').split(',').map((item) => item.trim);
|
||||||
|
if (prompt.includes('none')) {
|
||||||
|
prompt = ['none'];
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(setClient(resp.client));
|
dispatch(setClient(resp.client));
|
||||||
dispatch(setOAuthRequest(resp.oAuth));
|
dispatch(setOAuthRequest({
|
||||||
|
...resp.oAuth,
|
||||||
|
prompt: oauthData.prompt || 'none',
|
||||||
|
loginHint: oauthData.loginHint
|
||||||
|
}));
|
||||||
dispatch(setScopes(resp.session.scopes));
|
dispatch(setScopes(resp.session.scopes));
|
||||||
localStorage.setItem('oauthData', JSON.stringify({ // @see services/authFlow/AuthFlow
|
localStorage.setItem('oauthData', JSON.stringify({ // @see services/authFlow/AuthFlow
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@@ -246,6 +263,8 @@ export function setOAuthRequest(oauth) {
|
|||||||
redirectUrl: oauth.redirect_uri,
|
redirectUrl: oauth.redirect_uri,
|
||||||
responseType: oauth.response_type,
|
responseType: oauth.response_type,
|
||||||
scope: oauth.scope,
|
scope: oauth.scope,
|
||||||
|
prompt: oauth.prompt,
|
||||||
|
loginHint: oauth.loginHint,
|
||||||
state: oauth.state
|
state: oauth.state
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ function oauth(
|
|||||||
redirectUrl: payload.redirectUrl,
|
redirectUrl: payload.redirectUrl,
|
||||||
responseType: payload.responseType,
|
responseType: payload.responseType,
|
||||||
scope: payload.scope,
|
scope: payload.scope,
|
||||||
|
prompt: payload.prompt,
|
||||||
|
loginHint: payload.loginHint,
|
||||||
state: payload.state
|
state: payload.state
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,10 @@ function restoreScroll() {
|
|||||||
/* global process: false */
|
/* global process: false */
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
// some shortcuts for testing on localhost
|
// some shortcuts for testing on localhost
|
||||||
window.testOAuth = () => location.href = '/oauth2/v1/ely?client_id=ely&redirect_uri=http%3A%2F%2Fely.by%2Fauthorization%2Foauth&response_type=code&scope=account_info%2Caccount_email';
|
window.testOAuth = (loginHint = '') => location.href = `/oauth2/v1/ely?client_id=ely&redirect_uri=http%3A%2F%2Fely.by%2Fauthorization%2Foauth&response_type=code&scope=account_info%2Caccount_email&login_hint=${loginHint}`;
|
||||||
|
window.testOAuthPromptAccount = () => location.href = '/oauth2/v1/ely?client_id=ely&redirect_uri=http%3A%2F%2Fely.by%2Fauthorization%2Foauth&response_type=code&scope=account_info%2Caccount_email&prompt=select_account';
|
||||||
|
window.testOAuthPromptPermissions = (loginHint = '') => location.href = `/oauth2/v1/ely?client_id=ely&redirect_uri=http%3A%2F%2Fely.by%2Fauthorization%2Foauth&response_type=code&scope=account_info%2Caccount_email&prompt=consent&login_hint=${loginHint}`;
|
||||||
|
window.testOAuthPromptAll = () => location.href = '/oauth2/v1/ely?client_id=ely&redirect_uri=http%3A%2F%2Fely.by%2Fauthorization%2Foauth&response_type=code&scope=account_info%2Caccount_email&prompt=select_account,consent';
|
||||||
window.testOAuthStatic = () => location.href = '/oauth2/v1/ely?client_id=ely&redirect_uri=static_page_with_code&response_type=code&scope=account_info%2Caccount_email';
|
window.testOAuthStatic = () => location.href = '/oauth2/v1/ely?client_id=ely&redirect_uri=static_page_with_code&response_type=code&scope=account_info%2Caccount_email';
|
||||||
window.testOAuthStaticCode = () => location.href = '/oauth2/v1/ely?client_id=ely&redirect_uri=static_page&response_type=code&scope=account_info%2Caccount_email';
|
window.testOAuthStaticCode = () => location.href = '/oauth2/v1/ely?client_id=ely&redirect_uri=static_page&response_type=code&scope=account_info%2Caccount_email';
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ function getOAuthRequest(oauthData) {
|
|||||||
response_type: oauthData.responseType,
|
response_type: oauthData.responseType,
|
||||||
description: oauthData.description,
|
description: oauthData.description,
|
||||||
scope: oauthData.scope,
|
scope: oauthData.scope,
|
||||||
|
prompt: oauthData.prompt,
|
||||||
|
login_hint: oauthData.loginHint,
|
||||||
state: oauthData.state
|
state: oauthData.state
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,8 +192,8 @@ export default class AuthFlow {
|
|||||||
* @return {bool} - whether oauth state is being restored
|
* @return {bool} - whether oauth state is being restored
|
||||||
*/
|
*/
|
||||||
restoreOAuthState() {
|
restoreOAuthState() {
|
||||||
if (this.getRequest().path.indexOf('/register') === 0) {
|
if (/^\/(register|oauth2)/.test(this.getRequest().path)) {
|
||||||
// allow register
|
// allow register or the new oauth requests
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default class ChooseAccountState extends AbstractState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve(context, payload) {
|
resolve(context, payload) {
|
||||||
|
// do not ask again after user adds account, or chooses an existed one
|
||||||
context.run('setAccountSwitcher', false);
|
context.run('setAccountSwitcher', false);
|
||||||
|
|
||||||
if (payload.id) {
|
if (payload.id) {
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import ActivationState from './ActivationState';
|
|||||||
import AcceptRulesState from './AcceptRulesState';
|
import AcceptRulesState from './AcceptRulesState';
|
||||||
import FinishState from './FinishState';
|
import FinishState from './FinishState';
|
||||||
|
|
||||||
|
const PROMPT_ACCOUNT_CHOOSE = 'select_account';
|
||||||
|
const PROMPT_PERMISSIONS = 'consent';
|
||||||
|
|
||||||
export default class CompleteState extends AbstractState {
|
export default class CompleteState extends AbstractState {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
super(options);
|
super(options);
|
||||||
@@ -23,7 +26,33 @@ export default class CompleteState extends AbstractState {
|
|||||||
} else if (user.shouldAcceptRules) {
|
} else if (user.shouldAcceptRules) {
|
||||||
context.setState(new AcceptRulesState());
|
context.setState(new AcceptRulesState());
|
||||||
} else if (auth.oauth && auth.oauth.clientId) {
|
} else if (auth.oauth && auth.oauth.clientId) {
|
||||||
if (auth.isSwitcherEnabled && accounts.available.length > 1) {
|
let isSwitcherEnabled = auth.isSwitcherEnabled;
|
||||||
|
|
||||||
|
if (auth.oauth.loginHint) {
|
||||||
|
const account = accounts.available.filter((account) =>
|
||||||
|
account.id === auth.oauth.loginHint * 1
|
||||||
|
|| account.email === auth.oauth.loginHint
|
||||||
|
|| account.username === auth.oauth.loginHint
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
if (account) {
|
||||||
|
// disable switching, because we are know the account, user must be authorized with
|
||||||
|
context.run('setAccountSwitcher', false);
|
||||||
|
isSwitcherEnabled = false;
|
||||||
|
|
||||||
|
if (account.id !== accounts.active.id) {
|
||||||
|
// lets switch user to an account, that is needed for auth
|
||||||
|
return context.run('authenticate', account)
|
||||||
|
.then(() => context.setState(new CompleteState()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSwitcherEnabled
|
||||||
|
&& (accounts.available.length > 1
|
||||||
|
|| auth.oauth.prompt.includes(PROMPT_ACCOUNT_CHOOSE)
|
||||||
|
)
|
||||||
|
) {
|
||||||
context.setState(new ChooseAccountState());
|
context.setState(new ChooseAccountState());
|
||||||
} else if (auth.oauth.code) {
|
} else if (auth.oauth.code) {
|
||||||
context.setState(new FinishState());
|
context.setState(new FinishState());
|
||||||
@@ -31,7 +60,7 @@ export default class CompleteState extends AbstractState {
|
|||||||
const data = {};
|
const data = {};
|
||||||
if (typeof this.isPermissionsAccepted !== 'undefined') {
|
if (typeof this.isPermissionsAccepted !== 'undefined') {
|
||||||
data.accept = this.isPermissionsAccepted;
|
data.accept = this.isPermissionsAccepted;
|
||||||
} else if (auth.oauth.acceptRequired) {
|
} else if (auth.oauth.acceptRequired || auth.oauth.prompt.includes(PROMPT_PERMISSIONS)) {
|
||||||
context.setState(new PermissionsState());
|
context.setState(new PermissionsState());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export default class OAuthState extends AbstractState {
|
|||||||
responseType: query.response_type,
|
responseType: query.response_type,
|
||||||
description: query.description,
|
description: query.description,
|
||||||
scope: query.scope,
|
scope: query.scope,
|
||||||
|
prompt: query.prompt,
|
||||||
|
loginHint: query.login_hint,
|
||||||
state: query.state
|
state: query.state
|
||||||
}).then(() => context.setState(new CompleteState()));
|
}).then(() => context.setState(new CompleteState()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,11 @@ describe('components/auth/actions', () => {
|
|||||||
callThunk(oAuthValidate, oauthData).then(() => {
|
callThunk(oAuthValidate, oauthData).then(() => {
|
||||||
expectDispatchCalls([
|
expectDispatchCalls([
|
||||||
[setClient(resp.client)],
|
[setClient(resp.client)],
|
||||||
[setOAuthRequest(resp.oAuth)],
|
[setOAuthRequest({
|
||||||
|
...resp.oAuth,
|
||||||
|
prompt: 'none',
|
||||||
|
loginHint: undefined
|
||||||
|
})],
|
||||||
[setScopes(resp.session.scopes)]
|
[setScopes(resp.session.scopes)]
|
||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
@@ -102,7 +106,7 @@ describe('components/auth/actions', () => {
|
|||||||
|
|
||||||
return callThunk(oAuthComplete).then(() => {
|
return callThunk(oAuthComplete).then(() => {
|
||||||
expect(request.post, 'to have a call satisfying', [
|
expect(request.post, 'to have a call satisfying', [
|
||||||
'/api/oauth2/v1/complete?client_id=&redirect_uri=&response_type=&description=&scope=&state=',
|
'/api/oauth2/v1/complete?client_id=&redirect_uri=&response_type=&description=&scope=&prompt=&login_hint=&state=',
|
||||||
{}
|
{}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ describe('AuthFlow.functional', () => {
|
|||||||
|
|
||||||
auth: {
|
auth: {
|
||||||
oauth: {
|
oauth: {
|
||||||
clientId: 123
|
clientId: 123,
|
||||||
|
prompt: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
56
tests/services/authFlow/ChooseAccountState.test.js
Normal file
56
tests/services/authFlow/ChooseAccountState.test.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import ChooseAccountState from 'services/authFlow/ChooseAccountState';
|
||||||
|
import CompleteState from 'services/authFlow/CompleteState';
|
||||||
|
import LoginState from 'services/authFlow/LoginState';
|
||||||
|
|
||||||
|
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
|
||||||
|
|
||||||
|
describe('ChooseAccountState', () => {
|
||||||
|
let state;
|
||||||
|
let context;
|
||||||
|
let mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
state = new ChooseAccountState();
|
||||||
|
|
||||||
|
const data = bootstrap();
|
||||||
|
context = data.context;
|
||||||
|
mock = data.mock;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#enter', () => {
|
||||||
|
it('should navigate to /oauth/choose-account', () => {
|
||||||
|
expectNavigate(mock, '/oauth/choose-account');
|
||||||
|
|
||||||
|
state.enter(context);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#resolve', () => {
|
||||||
|
it('should transition to complete if existed account was choosen', () => {
|
||||||
|
expectRun(mock, 'setAccountSwitcher', false);
|
||||||
|
expectState(mock, CompleteState);
|
||||||
|
|
||||||
|
state.resolve(context, {id: 123});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transition to login if user wants to add new account', () => {
|
||||||
|
expectRun(mock, 'setAccountSwitcher', false);
|
||||||
|
expectNavigate(mock, '/login');
|
||||||
|
expectState(mock, LoginState);
|
||||||
|
|
||||||
|
state.resolve(context, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#reject', () => {
|
||||||
|
it('should logout', () => {
|
||||||
|
expectRun(mock, 'logout');
|
||||||
|
|
||||||
|
state.reject(context);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -144,7 +144,8 @@ describe('CompleteState', () => {
|
|||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
oauth: {
|
oauth: {
|
||||||
clientId: 'ely.by'
|
clientId: 'ely.by',
|
||||||
|
prompt: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -166,7 +167,8 @@ describe('CompleteState', () => {
|
|||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
oauth: {
|
oauth: {
|
||||||
clientId: 'ely.by'
|
clientId: 'ely.by',
|
||||||
|
prompt: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -194,7 +196,8 @@ describe('CompleteState', () => {
|
|||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
oauth: {
|
oauth: {
|
||||||
clientId: 'ely.by'
|
clientId: 'ely.by',
|
||||||
|
prompt: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -225,7 +228,8 @@ describe('CompleteState', () => {
|
|||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
oauth: {
|
oauth: {
|
||||||
clientId: 'ely.by'
|
clientId: 'ely.by',
|
||||||
|
prompt: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -242,21 +246,21 @@ describe('CompleteState', () => {
|
|||||||
return promise.catch(mock.verify.bind(mock));
|
return promise.catch(mock.verify.bind(mock));
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should transition to finish state if rejected with static_page', () => {
|
it('should transition to finish state if rejected with static_page', () =>
|
||||||
return testOAuth('resolve', {redirectUri: 'static_page'}, FinishState);
|
testOAuth('resolve', {redirectUri: 'static_page'}, FinishState)
|
||||||
});
|
);
|
||||||
|
|
||||||
it('should transition to finish state if rejected with static_page_with_code', () => {
|
it('should transition to finish state if rejected with static_page_with_code', () =>
|
||||||
return testOAuth('resolve', {redirectUri: 'static_page_with_code'}, FinishState);
|
testOAuth('resolve', {redirectUri: 'static_page_with_code'}, FinishState)
|
||||||
});
|
);
|
||||||
|
|
||||||
it('should transition to login state if rejected with unauthorized', () => {
|
it('should transition to login state if rejected with unauthorized', () =>
|
||||||
return testOAuth('reject', {unauthorized: true}, LoginState);
|
testOAuth('reject', {unauthorized: true}, LoginState)
|
||||||
});
|
);
|
||||||
|
|
||||||
it('should transition to permissions state if rejected with acceptRequired', () => {
|
it('should transition to permissions state if rejected with acceptRequired', () =>
|
||||||
return testOAuth('reject', {acceptRequired: true}, PermissionsState);
|
testOAuth('reject', {acceptRequired: true}, PermissionsState)
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('permissions accept', () => {
|
describe('permissions accept', () => {
|
||||||
@@ -285,7 +289,8 @@ describe('CompleteState', () => {
|
|||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
oauth: {
|
oauth: {
|
||||||
clientId: 'ely.by'
|
clientId: 'ely.by',
|
||||||
|
prompt: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -309,7 +314,8 @@ describe('CompleteState', () => {
|
|||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
oauth: {
|
oauth: {
|
||||||
clientId: 'ely.by'
|
clientId: 'ely.by',
|
||||||
|
prompt: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -337,6 +343,7 @@ describe('CompleteState', () => {
|
|||||||
auth: {
|
auth: {
|
||||||
oauth: {
|
oauth: {
|
||||||
clientId: 'ely.by',
|
clientId: 'ely.by',
|
||||||
|
prompt: [],
|
||||||
acceptRequired: true
|
acceptRequired: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,6 +372,7 @@ describe('CompleteState', () => {
|
|||||||
auth: {
|
auth: {
|
||||||
oauth: {
|
oauth: {
|
||||||
clientId: 'ely.by',
|
clientId: 'ely.by',
|
||||||
|
prompt: [],
|
||||||
acceptRequired: true
|
acceptRequired: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ describe('OAuthState', () => {
|
|||||||
response_type: 'response_type',
|
response_type: 'response_type',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
scope: 'scope',
|
scope: 'scope',
|
||||||
|
prompt: 'none',
|
||||||
|
login_hint: 1,
|
||||||
state: 'state'
|
state: 'state'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,6 +44,8 @@ describe('OAuthState', () => {
|
|||||||
responseType: query.response_type,
|
responseType: query.response_type,
|
||||||
description: query.description,
|
description: query.description,
|
||||||
scope: query.scope,
|
scope: query.scope,
|
||||||
|
prompt: query.prompt,
|
||||||
|
loginHint: query.login_hint,
|
||||||
state: query.state
|
state: query.state
|
||||||
})
|
})
|
||||||
).returns({then() {}});
|
).returns({then() {}});
|
||||||
|
|||||||
Reference in New Issue
Block a user