@truedat/auth 4.48.0 → 4.48.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,10 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.48.3] 2022-07-11
4
+
5
+ ### Changed
6
+
7
+ - [TD-3614] Refresh access token before expiry
8
+
9
+ ## [4.48.2] 2022-07-08
10
+
11
+ ### Changed
12
+
13
+ - [TD-4995] Support localization of template fields and fixed values
14
+
3
15
  ## [4.45.4] 2022-06-03
4
16
 
5
17
  ### Fixed
6
18
 
7
- - [TD-4873] Removed password and rep_password field from UserForm user update to avoid 403 error
19
+ - [TD-4873] Removed `password` and `rep_password` fields from `UserForm` user
20
+ update to avoid 403 error
8
21
 
9
22
  ## [4.44.4] 2022-05-19
10
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/auth",
3
- "version": "4.48.0",
3
+ "version": "4.48.4",
4
4
  "description": "Truedat Web Auth",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -87,7 +87,7 @@
87
87
  ]
88
88
  },
89
89
  "dependencies": {
90
- "@truedat/core": "4.48.0",
90
+ "@truedat/core": "4.48.4",
91
91
  "auth0-js": "^9.12.2",
92
92
  "immutable": "^4.0.0-rc.12",
93
93
  "jwt-decode": "^2.2.0",
@@ -109,5 +109,5 @@
109
109
  "react-dom": ">= 16.8.6 < 17",
110
110
  "semantic-ui-react": ">= 0.88.2 < 2.1"
111
111
  },
112
- "gitHead": "c4587d045fa6c9028082bdfbcfb35e4383f117ed"
112
+ "gitHead": "5e50a034469bf3ad8e02a64eb875ad2b710b6b03"
113
113
  }
@@ -2,4 +2,4 @@ export * from "./group";
2
2
  export * from "./groups";
3
3
  export * from "./groupsSearch";
4
4
  export * from "./groupRedirect";
5
- export * from "./groupUsers";
5
+ export { groupUsers } from "./groupUsers";
@@ -11,72 +11,50 @@ describe("reducers: authMessage", () => {
11
11
  });
12
12
 
13
13
  it("should handle the login.TRIGGER action", () => {
14
- expect(authMessage(fooState, { type: login.TRIGGER })).toEqual(fooState);
14
+ expect(authMessage(fooState, login())).toEqual(fooState);
15
15
  });
16
16
 
17
17
  it("should handle the login.FAILURE action", () => {
18
- expect(
19
- authMessage(fooState, {
20
- type: login.FAILURE
21
- })
22
- ).toEqual({
18
+ expect(authMessage(fooState, login.failure())).toEqual({
23
19
  error: true,
24
20
  icon: "attention",
25
21
  header: "alert.login.failed.header",
26
- content: "alert.login.failed.content"
22
+ content: "alert.login.failed.content",
27
23
  });
28
24
  });
29
25
 
30
26
  it("should handle the login.SUCCESS action", () => {
31
- expect(
32
- authMessage(fooState, {
33
- type: login.SUCCESS
34
- })
35
- ).toEqual(initialState);
27
+ expect(authMessage(fooState, login.success())).toEqual(initialState);
36
28
  });
37
29
 
38
30
  it("should handle the login.SUCCESS action", () => {
39
- expect(
40
- authMessage(fooState, {
41
- type: login.SUCCESS
42
- })
43
- ).toEqual(initialState);
31
+ expect(authMessage(fooState, login.success())).toEqual(initialState);
44
32
  });
45
33
 
46
34
  it("should handle the auth0Login.SUCCESS action", () => {
47
- expect(
48
- authMessage(fooState, {
49
- type: auth0Login.SUCCESS
50
- })
51
- ).toEqual(initialState);
35
+ expect(authMessage(fooState, auth0Login.success())).toEqual(initialState);
52
36
  });
53
37
 
54
38
  it("should handle the updatePassword.FAILURE status ", () => {
55
39
  expect(
56
- authMessage(fooState, {
57
- type: updatePassword.FAILURE,
58
- payload: "Failed with xxx"
59
- })
40
+ authMessage(fooState, updatePassword.failure("Failed with xxx"))
60
41
  ).toEqual({
61
42
  error: true,
62
43
  header: "user.password.error.xxx.header",
63
44
  content: "user.password.error.xxx.content",
64
- icon: "attention"
45
+ icon: "attention",
65
46
  });
66
47
  });
67
48
 
68
49
  it("should handle the updatePassword.SUCCESS with OK payload action", () => {
69
50
  expect(
70
- authMessage(fooState, {
71
- type: updatePassword.SUCCESS,
72
- payload: { foo: "bar" }
73
- })
51
+ authMessage(fooState, updatePassword.success({ foo: "bar" }))
74
52
  ).toEqual({
75
53
  error: false,
76
54
  header: "user.password.change",
77
55
  content: "user.password.success.change",
78
56
  icon: "check",
79
- color: "green"
57
+ color: "green",
80
58
  });
81
59
  });
82
60
  });
@@ -1,5 +1,11 @@
1
1
  import { dismissAlert } from "@truedat/core/routines";
2
- import { login, auth0Login, updatePassword } from "../routines";
2
+ import {
3
+ login,
4
+ auth0Login,
5
+ openIdLogin,
6
+ nonceLogin,
7
+ updatePassword,
8
+ } from "../routines";
3
9
 
4
10
  const initialState = {};
5
11
 
@@ -14,27 +20,44 @@ export const authMessage = (state = initialState, foo) => {
14
20
  header: "alert.auth0.failed.header",
15
21
  content: "alert.auth0.failed.content",
16
22
  icon: "attention",
17
- text: payload
23
+ text: payload,
18
24
  };
19
25
  case login.FAILURE:
20
26
  return {
21
27
  error: true,
22
28
  header: "alert.login.failed.header",
23
29
  content: "alert.login.failed.content",
24
- icon: "attention"
30
+ icon: "attention",
31
+ };
32
+ case openIdLogin.FAILURE:
33
+ return {
34
+ error: true,
35
+ header: "alert.login.failed.header",
36
+ content: "alert.login.failed.content",
37
+ icon: "attention",
38
+ };
39
+ case nonceLogin.FAILURE:
40
+ return {
41
+ error: true,
42
+ header: "alert.login.failed.header",
43
+ content: "alert.login.failed.content",
44
+ icon: "attention",
25
45
  };
26
46
  case login.SUCCESS:
27
47
  return initialState;
28
48
  case auth0Login.SUCCESS:
29
49
  return initialState;
50
+ case openIdLogin.SUCCESS:
51
+ return initialState;
52
+ case nonceLogin.SUCCESS:
53
+ return initialState;
30
54
  case updatePassword.FAILURE:
31
55
  const status = payload.substr(payload.length - 3);
32
-
33
56
  return {
34
57
  error: true,
35
58
  header: `user.password.error.${status}.header`,
36
59
  content: `user.password.error.${status}.content`,
37
- icon: "attention"
60
+ icon: "attention",
38
61
  };
39
62
  case updatePassword.SUCCESS:
40
63
  return {
@@ -42,7 +65,7 @@ export const authMessage = (state = initialState, foo) => {
42
65
  header: "user.password.change",
43
66
  content: "user.password.success.change",
44
67
  icon: "check",
45
- color: "green"
68
+ color: "green",
46
69
  };
47
70
  default:
48
71
  return state;
@@ -25,7 +25,10 @@ export const PermissionGroup = ({
25
25
  <List.Item key={i}>
26
26
  <Checkbox
27
27
  label={{
28
- children: formatMessage({ id: `permission.${p.name}` }),
28
+ children: formatMessage({
29
+ id: `permission.${p.name}`,
30
+ defaultMessage: p.name,
31
+ }),
29
32
  }}
30
33
  checked={_.any(_.propEq("id", p.id))(rolePermissions)}
31
34
  onChange={(e, data) => {
@@ -1,4 +1,5 @@
1
1
  const API_AUTH_METHODS = "/api/auth";
2
2
  const API_SESSIONS = "/api/sessions";
3
+ const API_SESSIONS_REFRESH = "/api/sessions/refresh";
3
4
 
4
- export { API_AUTH_METHODS, API_SESSIONS };
5
+ export { API_AUTH_METHODS, API_SESSIONS, API_SESSIONS_REFRESH };
@@ -1,24 +1,25 @@
1
+ import _ from "lodash/fp";
1
2
  import React from "react";
2
3
  import PropTypes from "prop-types";
3
4
  import { compose } from "redux";
4
5
  import { connect } from "react-redux";
5
6
  import { Route, Redirect, withRouter } from "react-router-dom";
6
7
  import { LOGIN, UNAUTHORIZED } from "@truedat/core/routes";
7
- import { REQUEST_TOKEN } from "../actions";
8
+ import { retrieveToken } from "../routines";
8
9
 
9
10
  class PrivateRoute extends React.Component {
10
11
  static propTypes = {
11
12
  token: PropTypes.string,
12
13
  component: PropTypes.func,
13
- has_permissions: PropTypes.bool,
14
+ hasPermissions: PropTypes.bool,
14
15
  location: PropTypes.object,
15
- readTokenFromStorage: PropTypes.func,
16
+ retrieveToken: PropTypes.func,
16
17
  };
17
18
 
18
19
  componentDidMount() {
19
- const { token, readTokenFromStorage } = this.props;
20
+ const { token, retrieveToken } = this.props;
20
21
  if (token === undefined) {
21
- readTokenFromStorage();
22
+ retrieveToken();
22
23
  }
23
24
  }
24
25
 
@@ -26,7 +27,7 @@ class PrivateRoute extends React.Component {
26
27
  const {
27
28
  component: Component,
28
29
  token,
29
- has_permissions,
30
+ hasPermissions,
30
31
  location,
31
32
  ...rest
32
33
  } = this.props;
@@ -35,7 +36,7 @@ class PrivateRoute extends React.Component {
35
36
  <Route
36
37
  {...rest}
37
38
  render={(props) => {
38
- if (token && !has_permissions) {
39
+ if (token && !hasPermissions) {
39
40
  return (
40
41
  <Redirect
41
42
  to={{
@@ -59,14 +60,10 @@ class PrivateRoute extends React.Component {
59
60
 
60
61
  const mapStateToProps = ({ authentication }) => ({
61
62
  token: authentication.token,
62
- has_permissions: authentication.has_permissions,
63
- });
64
-
65
- const mapDispatchToProps = (dispatch) => ({
66
- readTokenFromStorage: () => dispatch({ type: REQUEST_TOKEN }),
63
+ hasPermissions: !_.isEmpty(authentication.entitlements),
67
64
  });
68
65
 
69
66
  export default compose(
70
67
  withRouter,
71
- connect(mapStateToProps, mapDispatchToProps)
68
+ connect(mapStateToProps, { retrieveToken })
72
69
  )(PrivateRoute);
@@ -1,37 +1,33 @@
1
+ import _ from "lodash/fp";
1
2
  import React from "react";
2
3
  import PropTypes from "prop-types";
3
4
  import { connect } from "react-redux";
4
5
  import { Route, Redirect } from "react-router-dom";
5
6
  import { LOGIN } from "@truedat/core/routes";
6
- import { REQUEST_TOKEN } from "../actions";
7
+ import { retrieveToken } from "../routines";
7
8
 
8
9
  class UnauthorizedRoute extends React.Component {
9
10
  static propTypes = {
10
11
  token: PropTypes.string,
11
12
  component: PropTypes.func,
12
- readTokenFromStorage: PropTypes.func,
13
- has_permissions: PropTypes.bool
13
+ retrieveToken: PropTypes.func,
14
+ hasPermissions: PropTypes.bool,
14
15
  };
15
16
 
16
17
  componentDidMount() {
17
- const { token, readTokenFromStorage } = this.props;
18
+ const { token, retrieveToken } = this.props;
18
19
  if (token === undefined) {
19
- readTokenFromStorage();
20
+ retrieveToken();
20
21
  }
21
22
  }
22
23
 
23
24
  render() {
24
- const {
25
- component: Component,
26
- token,
27
- has_permissions,
28
- ...rest
29
- } = this.props;
25
+ const { component: Component, token, hasPermissions, ...rest } = this.props;
30
26
  return (
31
27
  <Route
32
28
  {...rest}
33
- render={props => {
34
- if (token && !has_permissions) {
29
+ render={(props) => {
30
+ if (token && !hasPermissions) {
35
31
  return <Component {...props} />;
36
32
  }
37
33
  return <Redirect to={{ pathname: LOGIN }} />;
@@ -43,11 +39,7 @@ class UnauthorizedRoute extends React.Component {
43
39
 
44
40
  const mapStateToProps = ({ authentication }) => ({
45
41
  token: authentication.token,
46
- has_permissions: authentication.has_permissions
42
+ hasPermissions: !_.isEmpty(authentication.entitlements),
47
43
  });
48
44
 
49
- const mapDispatchToProps = dispatch => ({
50
- readTokenFromStorage: () => dispatch({ type: REQUEST_TOKEN })
51
- });
52
-
53
- export default connect(mapStateToProps, mapDispatchToProps)(UnauthorizedRoute);
45
+ export default connect(mapStateToProps, { retrieveToken })(UnauthorizedRoute);
@@ -11,36 +11,30 @@ describe("reducers: authRedirect", () => {
11
11
  });
12
12
 
13
13
  it("should handle the clearRedirect.TRIGGER action", () => {
14
- expect(authRedirect(fooState, { type: clearRedirect.TRIGGER })).toEqual(
15
- initialState
16
- );
14
+ expect(authRedirect(fooState, clearRedirect())).toEqual(initialState);
17
15
  });
18
16
 
19
17
  it("should handle the auth0Login.FAILURE action", () => {
20
- expect(authRedirect(fooState, { type: auth0Login.FAILURE })).toEqual(
21
- "/login"
22
- );
18
+ expect(authRedirect(fooState, auth0Login.failure())).toEqual("/login");
23
19
  });
24
20
 
25
21
  it("should handle the auth0Login.SUCCESS action", () => {
26
- expect(authRedirect(fooState, { type: auth0Login.SUCCESS })).toEqual("");
22
+ expect(authRedirect(fooState, auth0Login.success())).toEqual("");
27
23
  });
28
24
 
29
25
  it("should handle the openIdLogin.FAILURE action", () => {
30
- expect(authRedirect(fooState, { type: openIdLogin.FAILURE })).toEqual(
31
- "/login"
32
- );
26
+ expect(authRedirect(fooState, openIdLogin.failure())).toEqual("/login");
33
27
  });
34
28
 
35
29
  it("should handle the openIdLogin.SUCCESS action", () => {
36
- expect(authRedirect(fooState, { type: openIdLogin.SUCCESS })).toEqual("");
30
+ expect(authRedirect(fooState, openIdLogin.success())).toEqual("");
37
31
  });
38
32
 
39
- it("should handle FAILURES with status 401 payload", () => {
33
+ it("should handle FAILURES with status 401 status and unauthorized message", () => {
40
34
  expect(
41
35
  authRedirect(fooState, {
42
36
  type: "random/FAILURE",
43
- payload: { status: 401 },
37
+ payload: { status: 401, data: { message: "unauthorized" } },
44
38
  })
45
39
  ).toEqual("/login");
46
40
  });
@@ -1,6 +1,9 @@
1
- import { authentication, loginSubmitting } from "../../reducers";
2
- import { login } from "../../routines";
3
- import { RECEIVE_LOGOUT, RECEIVE_TOKEN } from "../../actions";
1
+ import {
2
+ authentication,
3
+ loginSubmitting,
4
+ initialState,
5
+ } from "../authentication";
6
+ import { login, logout, retrieveToken } from "../../routines";
4
7
 
5
8
  const fooState = { foo: "bar" };
6
9
 
@@ -23,44 +26,34 @@ describe("reducers: loginSubmitting", () => {
23
26
  });
24
27
 
25
28
  describe("reducers: authentication", () => {
26
- const initialState = {
27
- token: undefined,
28
- role: undefined,
29
- has_permissions: false,
30
- groups: []
31
- };
32
29
  it("should provide the initial state", () => {
33
- expect(authentication(undefined, {})).toEqual(initialState);
30
+ expect(authentication(undefined, {})).toBe(initialState);
34
31
  });
35
32
 
36
33
  it("should handle the SUCCESS action", () => {
37
34
  const payload = { token: "TOKEN", refresh_token: "REFRESH" };
38
35
  expect(
39
- authentication({ foo: "bar" }, { type: login.SUCCESS, payload })
36
+ authentication({ foo: "bar" }, login.success(payload))
40
37
  ).toMatchObject({ token: "TOKEN" });
41
38
  });
42
39
 
43
40
  it("should handle the FAILURE action", () => {
44
- expect(authentication({ foo: "bar" }, { type: login.FAILURE })).toEqual(
45
- initialState
46
- );
41
+ expect(authentication({ foo: "bar" }, login.failure())).toBe(initialState);
47
42
  });
48
43
 
49
- it("should handle the RECEIVE_LOGOUT action", () => {
50
- expect(authentication({ foo: "bar" }, { type: RECEIVE_LOGOUT })).toEqual(
51
- initialState
52
- );
44
+ it("should handle the logout.FULFILL action", () => {
45
+ expect(authentication({ foo: "bar" }, logout.fulfill())).toBe(initialState);
53
46
  });
54
47
 
55
- it("should handle the RECEIVE_TOKEN action", () => {
48
+ it("should handle the retrieveToken.SUCCESS action", () => {
56
49
  expect(
57
- authentication({ foo: "bar" }, { type: RECEIVE_TOKEN, token: "token" })
50
+ authentication({ foo: "bar" }, retrieveToken.success({ token: "token" }))
58
51
  ).toHaveProperty("token");
59
52
  });
60
53
 
61
54
  it("should ignore unknown actions", () => {
62
55
  expect(authentication({ foo: "bar" }, { type: "WTF" })).toEqual({
63
- foo: "bar"
56
+ foo: "bar",
64
57
  });
65
58
  });
66
59
  });
@@ -21,7 +21,11 @@ const authRedirect = (state = initialState, { type, payload }) => {
21
21
  case nonceLogin.SUCCESS:
22
22
  return "/";
23
23
  default:
24
- if (_.endsWith("/FAILURE")(type) && _.prop("status")(payload) === 401) {
24
+ if (
25
+ _.endsWith("/FAILURE")(type) &&
26
+ payload?.status === 401 &&
27
+ payload?.data?.message === "unauthorized"
28
+ ) {
25
29
  return "/login";
26
30
  }
27
31
  return state;
@@ -1,6 +1,12 @@
1
1
  import _ from "lodash/fp";
2
- import { login, auth0Login, openIdLogin } from "../routines";
3
- import { RECEIVE_LOGOUT, RECEIVE_TOKEN } from "../actions";
2
+ import {
3
+ login,
4
+ auth0Login,
5
+ openIdLogin,
6
+ nonceLogin,
7
+ logout,
8
+ } from "../routines";
9
+ import { retrieveToken } from "../routines";
4
10
 
5
11
  const loginSubmitting = (state = false, { type }) => {
6
12
  switch (type) {
@@ -13,80 +19,56 @@ const loginSubmitting = (state = false, { type }) => {
13
19
  }
14
20
  };
15
21
 
16
- const initialState = {
22
+ export const initialState = {
17
23
  auth_realm: undefined,
18
24
  token: undefined,
19
25
  role: undefined,
20
- has_permissions: false,
26
+ entitlements: undefined,
21
27
  groups: [],
22
28
  };
23
29
 
24
- const authentication = (state = initialState, { type, payload, ...data }) => {
30
+ const TOKEN_PROPS = [
31
+ "amr",
32
+ "groups",
33
+ "entitlements",
34
+ "role",
35
+ "token",
36
+ "user_name",
37
+ ];
38
+
39
+ const authentication = (state = initialState, { type, payload }) => {
25
40
  switch (type) {
26
41
  case login.TRIGGER:
27
42
  const { auth_realm } = payload;
28
43
  return { ...initialState, auth_realm };
29
- case openIdLogin.SUCCESS:
30
- return Object.assign(
31
- {},
32
- state,
33
- _.pick([
34
- "user_name",
35
- "token",
36
- "role",
37
- "has_permissions",
38
- "remember",
39
- "access_method",
40
- "groups",
41
- ])(payload)
42
- );
43
44
  case auth0Login.SUCCESS:
44
- return Object.assign(
45
- {},
46
- state,
47
- _.pick([
48
- "user_name",
49
- "token",
50
- "role",
51
- "has_permissions",
52
- "remember",
53
- "access_method",
54
- "groups",
55
- ])(payload)
56
- );
45
+ return Object.assign({}, state, _.pick(TOKEN_PROPS)(payload));
57
46
  case login.SUCCESS:
58
- return Object.assign(
59
- {},
60
- state,
61
- _.pick([
62
- "user_name",
63
- "token",
64
- "role",
65
- "has_permissions",
66
- "remember",
67
- "access_method",
68
- "groups",
69
- ])(payload)
70
- );
47
+ return Object.assign({}, state, _.pick(TOKEN_PROPS)(payload));
48
+ case nonceLogin.SUCCESS:
49
+ return Object.assign({}, state, _.pick(TOKEN_PROPS)(payload));
50
+ case openIdLogin.SUCCESS:
51
+ return Object.assign({}, state, _.pick(TOKEN_PROPS)(payload));
52
+ case retrieveToken.SUCCESS:
53
+ return Object.assign({}, state, _.pick(TOKEN_PROPS)(payload));
54
+ case auth0Login.FAILURE:
55
+ return initialState;
71
56
  case login.FAILURE:
72
57
  return initialState;
73
- case RECEIVE_TOKEN:
74
- return Object.assign(
75
- {},
76
- state,
77
- _.pick([
78
- "user_name",
79
- "token",
80
- "role",
81
- "has_permissions",
82
- "remember",
83
- "access_method",
84
- "groups",
85
- ])(data)
86
- );
87
- case RECEIVE_LOGOUT:
58
+ case nonceLogin.FAILURE:
59
+ return initialState;
60
+ case openIdLogin.FAILURE:
61
+ return initialState;
62
+ case logout.FULFILL:
88
63
  return initialState;
89
64
  default:
65
+ if (
66
+ _.endsWith("/FAILURE")(type) &&
67
+ payload?.status === 401 &&
68
+ payload?.data?.message === "unauthorized"
69
+ ) {
70
+ return initialState;
71
+ }
90
72
  return state;
91
73
  }
92
74
  };
@@ -1,7 +1,10 @@
1
1
  import { createRoutine } from "redux-saga-routines";
2
2
 
3
3
  export const login = createRoutine("LOGIN");
4
+ export const logout = createRoutine("LOGOUT");
4
5
  export const auth0Login = createRoutine("AUTH0");
5
6
  export const openIdLogin = createRoutine("OPENID");
6
7
  export const nonceLogin = createRoutine("NONCE");
7
8
  export const fetchAuthMethods = createRoutine("FETCH_AUTH_METHODS");
9
+ export const refreshSession = createRoutine("REFRESH_SESSION");
10
+ export const retrieveToken = createRoutine("READ_TOKEN");
@@ -38,17 +38,17 @@ describe("sagas: post login", () => {
38
38
  it("should handle postLoginSaga when a response is returned", () => {
39
39
  const user_name = "fulanito.menganito@bluetab.net";
40
40
  const password = "top_secret";
41
- const access_method = "alternative_login";
41
+ const access_method = "pwd";
42
42
  const token = "token";
43
- const has_permissions = true;
43
+ const entitlements = ["p"];
44
44
  const token_username = "some_username";
45
45
  const role = "user";
46
- const type = "type";
47
46
  const exp = "exp";
48
47
  const pre_user = { user_name, password };
49
48
  const user = { user_name, password };
50
49
  const data = { token };
51
50
  const groups = ["foo"];
51
+ const amr = ["pwd"];
52
52
 
53
53
  expect(() => {
54
54
  testSaga(postLoginSaga, { payload: pre_user, access_method })
@@ -61,24 +61,22 @@ describe("sagas: post login", () => {
61
61
  .next()
62
62
  .call(jwt_decode, token)
63
63
  .next({
64
- user_name: token_username,
65
- role,
66
- has_permissions,
67
- access_method,
68
- type,
64
+ amr,
65
+ entitlements,
69
66
  exp,
70
67
  groups,
68
+ role,
69
+ user_name: token_username,
71
70
  })
72
71
  .put(
73
72
  login.success({
74
- user_name: token_username,
75
- token,
76
- role,
77
- has_permissions,
78
- access_method,
79
- type,
73
+ amr,
74
+ entitlements,
80
75
  exp,
81
76
  groups,
77
+ role,
78
+ token,
79
+ user_name: token_username,
82
80
  })
83
81
  )
84
82
  .next()
@@ -95,7 +93,7 @@ describe("sagas: post login", () => {
95
93
  const user = { user_name: username, password };
96
94
  const errorData = "Login failed";
97
95
  const error = { response: { data: errorData } };
98
- const access_method = "alternative_login";
96
+ const access_method = "pwd";
99
97
 
100
98
  expect(() => {
101
99
  testSaga(postLoginSaga, { payload: user })
@@ -119,10 +117,10 @@ describe("sagas: openIdLogin", () => {
119
117
  const state = "bar";
120
118
  const payload = { code, state };
121
119
 
120
+ const amr = [];
122
121
  const user_name = "some_username";
123
122
  const role = "user";
124
- const has_permissions = true;
125
- const type = "type";
123
+ const entitlements = ["p"];
126
124
  const exp = "exp";
127
125
  const groups = ["foo"];
128
126
  const token = "token";
@@ -140,22 +138,22 @@ describe("sagas: openIdLogin", () => {
140
138
  .next()
141
139
  .call(jwt_decode, token)
142
140
  .next({
143
- user_name,
144
- role,
145
- has_permissions,
146
- type,
141
+ amr,
142
+ entitlements,
147
143
  exp,
148
144
  groups,
145
+ role,
146
+ user_name,
149
147
  })
150
148
  .put(
151
149
  openIdLogin.success({
152
- user_name,
153
- token,
154
- role,
155
- has_permissions,
156
- type,
150
+ amr,
151
+ entitlements,
157
152
  exp,
158
153
  groups,
154
+ role,
155
+ token,
156
+ user_name,
159
157
  })
160
158
  )
161
159
  .next()
@@ -170,10 +168,10 @@ describe("sagas: openIdLogin", () => {
170
168
  const state = "bar";
171
169
  const payload = { id_token, state };
172
170
 
171
+ const amr = [];
173
172
  const user_name = "some_username";
174
173
  const role = "user";
175
- const has_permissions = true;
176
- const type = "type";
174
+ const entitlements = [];
177
175
  const exp = "exp";
178
176
  const groups = ["foo"];
179
177
  const token = "token";
@@ -196,22 +194,22 @@ describe("sagas: openIdLogin", () => {
196
194
  .next()
197
195
  .call(jwt_decode, token)
198
196
  .next({
199
- user_name,
200
- role,
201
- has_permissions,
202
- type,
197
+ amr,
198
+ entitlements,
203
199
  exp,
204
200
  groups,
201
+ role,
202
+ user_name,
205
203
  })
206
204
  .put(
207
205
  openIdLogin.success({
208
- user_name,
209
- token,
210
- role,
211
- has_permissions,
212
- type,
206
+ amr,
207
+ entitlements,
213
208
  exp,
214
209
  groups,
210
+ role,
211
+ token,
212
+ user_name,
215
213
  })
216
214
  )
217
215
  .next()
@@ -1,14 +1,16 @@
1
1
  import { testSaga } from "redux-saga-test-plan";
2
+ import { apiJsonDelete, JSON_OPTS } from "@truedat/core/services/api";
2
3
  import { clearToken } from "@truedat/core/services/storage";
4
+ import { API_SESSIONS } from "../../api";
3
5
  import { logoutSaga, logoutRequestSaga } from "../logout";
4
- import { REQUEST_LOGOUT, RECEIVE_LOGOUT } from "../../actions";
6
+ import { logout } from "../../routines";
5
7
 
6
8
  describe("sagas: logout request", () => {
7
- it("should success handling loginRequestSaga", () => {
9
+ it("should success handling logoutRequestSaga", () => {
8
10
  expect(() => {
9
11
  testSaga(logoutRequestSaga)
10
12
  .next()
11
- .takeLatest(REQUEST_LOGOUT, logoutSaga)
13
+ .takeLatest(logout.TRIGGER, logoutSaga)
12
14
  .finish()
13
15
  .isDone();
14
16
  }).not.toThrow();
@@ -22,13 +24,20 @@ describe("sagas: logout request", () => {
22
24
  });
23
25
 
24
26
  describe("sagas: logoutSaga", () => {
25
- it("should call clearToken", () => {
27
+ it("should call DELETE /api/sessions and clearToken", () => {
28
+ const data = "";
26
29
  expect(() => {
27
30
  testSaga(logoutSaga)
31
+ .next()
32
+ .put(logout.request())
33
+ .next()
34
+ .call(apiJsonDelete, API_SESSIONS, JSON_OPTS)
35
+ .next({ data })
36
+ .put(logout.success(data))
28
37
  .next()
29
38
  .call(clearToken)
30
39
  .next()
31
- .put({ type: RECEIVE_LOGOUT })
40
+ .put(logout.fulfill())
32
41
  .next()
33
42
  .isDone();
34
43
  }).not.toThrow();
@@ -0,0 +1,87 @@
1
+ import jwt_decode from "jwt-decode";
2
+ import { testSaga } from "redux-saga-test-plan";
3
+ import { takeLatest } from "redux-saga/effects";
4
+ import { apiJsonPost, JSON_OPTS } from "@truedat/core/services/api";
5
+ import { saveToken } from "@truedat/core/services/storage";
6
+ import { API_SESSIONS_REFRESH } from "../../api";
7
+ import { refreshDelay, refreshSaga, refreshRequestSaga } from "../refresh";
8
+ import {
9
+ login,
10
+ auth0Login,
11
+ openIdLogin,
12
+ nonceLogin,
13
+ refreshSession,
14
+ retrieveToken,
15
+ } from "../../routines";
16
+
17
+ describe("sagas: refresh request", () => {
18
+ it(`should success handling login success, etc.`, () => {
19
+ expect(() => {
20
+ testSaga(refreshRequestSaga)
21
+ .next()
22
+ .all([
23
+ takeLatest(login.SUCCESS, refreshSaga),
24
+ takeLatest(auth0Login.SUCCESS, refreshSaga),
25
+ takeLatest(openIdLogin.SUCCESS, refreshSaga),
26
+ takeLatest(nonceLogin.SUCCESS, refreshSaga),
27
+ takeLatest(refreshSession.SUCCESS, refreshSaga),
28
+ takeLatest(retrieveToken.SUCCESS, refreshSaga),
29
+ ])
30
+ .finish()
31
+ .isDone();
32
+ }).not.toThrow();
33
+ });
34
+
35
+ it("should fail handling refreshRequestSaga if wrong pattern", () => {
36
+ expect(() => {
37
+ testSaga(refreshRequestSaga)
38
+ .next()
39
+ .takeLatest("WTF_PATTERN", refreshSaga);
40
+ }).toThrow();
41
+ });
42
+ });
43
+
44
+ describe("sagas: refreshSaga", () => {
45
+ it("should call POST /api/sessions/refresh and saveToken", () => {
46
+ const amr = "pwd";
47
+ const entitlements = ["p"];
48
+ const groups = [];
49
+ const role = "user";
50
+ const user_name = "user_name";
51
+ const exp = 1657026;
52
+ const payload = { exp };
53
+ const token = "token";
54
+ const data = { token };
55
+ expect(() => {
56
+ testSaga(refreshSaga, { payload })
57
+ .next()
58
+ .call(refreshDelay, payload)
59
+ .next(5000)
60
+ .delay(5000)
61
+ .next()
62
+ .put(refreshSession.request(payload))
63
+ .next()
64
+ .call(apiJsonPost, API_SESSIONS_REFRESH, {}, JSON_OPTS)
65
+ .next({ data })
66
+ .call(saveToken, token)
67
+ .next()
68
+ .call(jwt_decode, token)
69
+ .next({ amr, entitlements, exp, groups, role, user_name })
70
+ .put(
71
+ refreshSession.success({
72
+ amr,
73
+ entitlements,
74
+ exp,
75
+ groups,
76
+ role,
77
+ user_name,
78
+ token,
79
+ })
80
+ )
81
+ .next()
82
+ .put(refreshSession.fulfill())
83
+ .next()
84
+ .isDone();
85
+ }).not.toThrow();
86
+ });
87
+ });
@@ -1,11 +1,12 @@
1
+ import _ from "lodash/fp";
1
2
  import jwt_decode from "jwt-decode";
2
3
  import { testSaga } from "redux-saga-test-plan";
3
4
  import { readToken as readTokenFromStorage } from "@truedat/core/services/storage";
4
5
  import { tokenRequestSaga, checkExpired, readToken } from "../token";
5
- import { REQUEST_TOKEN, RECEIVE_TOKEN } from "../../actions";
6
+ import { retrieveToken } from "../../routines";
6
7
 
7
8
  describe("sagas: check expired", () => {
8
- const now = Date.now().valueOf() / 1000;
9
+ const now = _.toInteger(Date.now().valueOf() / 1000);
9
10
  const oneMinuteAgo = now - 60;
10
11
  const oneMinuteFromNow = now + 60;
11
12
 
@@ -27,13 +28,13 @@ describe("sagas: check expired", () => {
27
28
  });
28
29
 
29
30
  describe("sagas: read token", () => {
30
- const exp = Date.now().valueOf() / 1000 + 60;
31
+ const exp = _.toInteger(Date.now().valueOf() / 1000 + 60);
31
32
  const user_name = "user";
32
33
  const token = "TOKEN";
33
34
  const role = "user";
34
- const has_permissions = false;
35
- const access_method = undefined;
35
+ const amr = undefined;
36
36
  const groups = ["business_glossary"];
37
+ const entitlements = ["p"];
37
38
 
38
39
  it("should call readTokenFromStorage, jwt_decode and checkExpired", () => {
39
40
  expect(() => {
@@ -42,18 +43,20 @@ describe("sagas: read token", () => {
42
43
  .call(readTokenFromStorage)
43
44
  .next(token)
44
45
  .call(jwt_decode, token)
45
- .next({ user_name, exp, role, has_permissions, groups })
46
+ .next({ user_name, entitlements, exp, role, groups })
46
47
  .call(checkExpired, exp)
47
48
  .next(false)
48
- .put({
49
- type: RECEIVE_TOKEN,
50
- user_name,
51
- token,
52
- role,
53
- has_permissions,
54
- access_method,
55
- groups,
56
- })
49
+ .put(
50
+ retrieveToken.success({
51
+ amr,
52
+ entitlements,
53
+ exp,
54
+ groups,
55
+ role,
56
+ token,
57
+ user_name,
58
+ })
59
+ )
57
60
  .next()
58
61
  .isDone();
59
62
  }).not.toThrow();
@@ -89,7 +92,7 @@ describe("sagas: token request", () => {
89
92
  expect(() => {
90
93
  testSaga(tokenRequestSaga)
91
94
  .next()
92
- .takeLatest(REQUEST_TOKEN, readToken)
95
+ .takeLatest(retrieveToken.TRIGGER, readToken)
93
96
  .finish()
94
97
  .isDone();
95
98
  }).not.toThrow();
@@ -2,17 +2,20 @@ import { fetchAuthMethodsRequestSaga } from "./fetchAuthMethods";
2
2
  import { loginRequestSaga } from "./login";
3
3
  import { logoutRequestSaga } from "./logout";
4
4
  import { tokenRequestSaga } from "./token";
5
+ import { refreshRequestSaga } from "./refresh";
5
6
 
6
7
  export {
7
8
  fetchAuthMethodsRequestSaga,
8
9
  loginRequestSaga,
9
10
  logoutRequestSaga,
10
- tokenRequestSaga
11
+ tokenRequestSaga,
12
+ refreshRequestSaga,
11
13
  };
12
14
 
13
15
  export default [
14
16
  fetchAuthMethodsRequestSaga(),
15
17
  loginRequestSaga(),
16
18
  logoutRequestSaga(),
17
- tokenRequestSaga()
19
+ tokenRequestSaga(),
20
+ refreshRequestSaga(),
18
21
  ];
@@ -23,18 +23,18 @@ export function* auth0LoginSaga({ payload }) {
23
23
  const { data } = yield call(apiJsonPost, url, body, opts);
24
24
  const { token } = data;
25
25
  yield call(saveToken, token);
26
- const { user_name, has_permissions, type, exp, role, groups } = yield call(
26
+ const { amr, entitlements, exp, groups, role, user_name } = yield call(
27
27
  jwt_decode,
28
28
  token
29
29
  );
30
30
  yield put(
31
31
  auth0Login.success({
32
- user_name,
33
- type,
32
+ amr,
33
+ entitlements,
34
34
  exp,
35
- has_permissions,
36
35
  groups,
37
36
  role,
37
+ user_name,
38
38
  ...data,
39
39
  })
40
40
  );
@@ -58,19 +58,18 @@ export function* openIdLoginSaga({ payload }) {
58
58
  const { data } = yield call(apiJsonPost, url, body, opts);
59
59
  const { token } = data;
60
60
  yield call(saveToken, token);
61
- const { user_name, has_permissions, type, exp, role, groups } = yield call(
61
+ const { amr, entitlements, exp, groups, role, user_name } = yield call(
62
62
  jwt_decode,
63
63
  token
64
64
  );
65
- // TODO: push username to GTM dataLayer
66
65
  yield put(
67
66
  openIdLogin.success({
68
- user_name,
69
- type,
67
+ amr,
68
+ entitlements,
70
69
  exp,
71
- has_permissions,
72
- role,
73
70
  groups,
71
+ role,
72
+ user_name,
74
73
  ...data,
75
74
  })
76
75
  );
@@ -90,18 +89,18 @@ export function* nonceLoginSaga({ payload }) {
90
89
  const { data } = yield call(apiJsonPost, url, body, JSON_OPTS);
91
90
  const { token } = data;
92
91
  yield call(saveToken, token);
93
- const { user_name, has_permissions, type, exp, role, groups } = yield call(
92
+ const { amr, entitlements, exp, groups, role, user_name } = yield call(
94
93
  jwt_decode,
95
94
  token
96
95
  );
97
96
  yield put(
98
97
  nonceLogin.success({
99
- user_name,
100
- type,
98
+ amr,
99
+ entitlements,
101
100
  exp,
102
- has_permissions,
103
- role,
104
101
  groups,
102
+ role,
103
+ user_name,
105
104
  ...data,
106
105
  })
107
106
  );
@@ -118,30 +117,24 @@ export function* postLoginSaga({ payload }) {
118
117
  const auth_realm = _.prop("auth_realm")(payload);
119
118
  const user = _.pick(["user_name", "password"])(payload);
120
119
  const requestData = auth_realm
121
- ? { access_method: "alternative_login", auth_realm, user }
122
- : { access_method: "alternative_login", user };
120
+ ? { access_method: "pwd", auth_realm, user }
121
+ : { access_method: "pwd", user };
123
122
  yield put(login.request());
124
123
  const { data } = yield call(apiJsonPost, url, requestData, JSON_OPTS);
125
124
  const { token } = data;
126
125
  yield call(saveToken, token);
127
- const {
128
- user_name,
129
- has_permissions,
130
- type,
131
- exp,
132
- role,
133
- groups,
134
- access_method,
135
- } = yield call(jwt_decode, token);
126
+ const { amr, entitlements, exp, groups, role, user_name } = yield call(
127
+ jwt_decode,
128
+ token
129
+ );
136
130
  yield put(
137
131
  login.success({
138
- user_name,
139
- has_permissions,
140
- type,
132
+ amr,
133
+ entitlements,
141
134
  exp,
142
- role,
143
135
  groups,
144
- access_method,
136
+ role,
137
+ user_name,
145
138
  ...data,
146
139
  })
147
140
  );
@@ -1,12 +1,27 @@
1
- import { call, put, takeLatest } from "redux-saga/effects";
1
+ import { all, call, put, takeLatest } from "redux-saga/effects";
2
+ import { apiJsonDelete, JSON_OPTS } from "@truedat/core/services/api";
2
3
  import { clearToken } from "@truedat/core/services/storage";
3
- import { REQUEST_LOGOUT, RECEIVE_LOGOUT } from "../actions";
4
+ import { API_SESSIONS } from "../api";
5
+ import { logout } from "../routines";
4
6
 
5
7
  export function* logoutSaga() {
6
- yield call(clearToken);
7
- yield put({ type: RECEIVE_LOGOUT });
8
+ try {
9
+ yield put(logout.request());
10
+ const { data } = yield call(apiJsonDelete, API_SESSIONS, JSON_OPTS);
11
+ yield put(logout.success(data));
12
+ } catch (error) {
13
+ if (error.response) {
14
+ const { status, data } = error.response;
15
+ yield put(logout.failure({ status, data }));
16
+ } else {
17
+ yield put(logout.failure(error.message));
18
+ }
19
+ } finally {
20
+ yield call(clearToken);
21
+ yield put(logout.fulfill());
22
+ }
8
23
  }
9
24
 
10
25
  export function* logoutRequestSaga() {
11
- yield takeLatest(REQUEST_LOGOUT, logoutSaga);
26
+ yield takeLatest(logout.TRIGGER, logoutSaga);
12
27
  }
@@ -0,0 +1,66 @@
1
+ import jwt_decode from "jwt-decode";
2
+ import { all, call, put, takeLatest, delay } from "redux-saga/effects";
3
+ import { apiJsonPost, JSON_OPTS } from "@truedat/core/services/api";
4
+ import { saveToken } from "@truedat/core/services/storage";
5
+ import {
6
+ login,
7
+ auth0Login,
8
+ openIdLogin,
9
+ nonceLogin,
10
+ refreshSession,
11
+ retrieveToken,
12
+ } from "../routines";
13
+ import { API_SESSIONS_REFRESH } from "../api";
14
+
15
+ // 5 seconds
16
+ const MIN_DELAY = 5000;
17
+
18
+ // milliseconds until 1 minute before expiry, but at least MIN_DELAY
19
+ export const refreshDelay = ({ exp }) =>
20
+ exp ? Math.max(MIN_DELAY, exp * 1000 - Date.now() - 60000) : MIN_DELAY;
21
+
22
+ export function* refreshSaga({ payload }) {
23
+ const millis = yield call(refreshDelay, payload || {});
24
+ yield delay(millis);
25
+ yield put(refreshSession.request(payload));
26
+ try {
27
+ const { data } = yield call(
28
+ apiJsonPost,
29
+ API_SESSIONS_REFRESH,
30
+ {},
31
+ JSON_OPTS
32
+ );
33
+ const { token } = data;
34
+ yield call(saveToken, token);
35
+ const { amr, entitlements, exp, groups, role, user_name } = yield call(
36
+ jwt_decode,
37
+ token
38
+ );
39
+ yield put(
40
+ refreshSession.success({
41
+ amr,
42
+ entitlements,
43
+ exp,
44
+ groups,
45
+ role,
46
+ user_name,
47
+ ...data,
48
+ })
49
+ );
50
+ } catch (error) {
51
+ yield put(refreshSession.failure(error.message));
52
+ } finally {
53
+ yield put(refreshSession.fulfill());
54
+ }
55
+ }
56
+
57
+ export function* refreshRequestSaga() {
58
+ yield all([
59
+ takeLatest(login.SUCCESS, refreshSaga),
60
+ takeLatest(auth0Login.SUCCESS, refreshSaga),
61
+ takeLatest(openIdLogin.SUCCESS, refreshSaga),
62
+ takeLatest(nonceLogin.SUCCESS, refreshSaga),
63
+ takeLatest(refreshSession.SUCCESS, refreshSaga),
64
+ takeLatest(retrieveToken.SUCCESS, refreshSaga),
65
+ ]);
66
+ }
@@ -1,7 +1,7 @@
1
1
  import jwt_decode from "jwt-decode";
2
2
  import { call, put, takeLatest } from "redux-saga/effects";
3
3
  import { readToken as readTokenFromStorage } from "@truedat/core/services/storage";
4
- import { REQUEST_TOKEN, RECEIVE_TOKEN } from "../actions";
4
+ import { retrieveToken } from "../routines";
5
5
 
6
6
  export const checkExpired = (exp) => {
7
7
  if (exp) {
@@ -15,23 +15,27 @@ export const checkExpired = (exp) => {
15
15
  export function* readToken() {
16
16
  const token = yield call(readTokenFromStorage);
17
17
  if (token) {
18
- const { user_name, exp, role, has_permissions, access_method, groups } =
19
- yield call(jwt_decode, token);
18
+ const { amr, entitlements, exp, groups, role, user_name } = yield call(
19
+ jwt_decode,
20
+ token
21
+ );
20
22
  const isExpired = yield call(checkExpired, exp);
21
23
  if (!isExpired) {
22
- yield put({
23
- type: RECEIVE_TOKEN,
24
- user_name,
25
- has_permissions,
26
- token,
27
- role,
28
- access_method,
29
- groups,
30
- });
24
+ yield put(
25
+ retrieveToken.success({
26
+ amr,
27
+ entitlements,
28
+ exp,
29
+ groups,
30
+ role,
31
+ token,
32
+ user_name,
33
+ })
34
+ );
31
35
  }
32
36
  }
33
37
  }
34
38
 
35
39
  export function* tokenRequestSaga() {
36
- yield takeLatest(REQUEST_TOKEN, readToken);
40
+ yield takeLatest(retrieveToken.TRIGGER, readToken);
37
41
  }
@@ -17,9 +17,7 @@ describe("sagas: fetchUsersRequestSaga", () => {
17
17
 
18
18
  it("should throw exception if an unhandled action is received", () => {
19
19
  expect(() => {
20
- testSaga(fetchUsersRequestSaga)
21
- .next()
22
- .takeLatest("FOO", fetchUsersSaga);
20
+ testSaga(fetchUsersRequestSaga).next().takeLatest("FOO", fetchUsersSaga);
23
21
  }).toThrow();
24
22
  });
25
23
  });
@@ -28,8 +26,8 @@ describe("sagas: fetchUsersSaga", () => {
28
26
  const data = {
29
27
  collection: [
30
28
  { id: 1, name: "User 1", email: "user1@truedat.net" },
31
- { id: 2, name: "User 2", email: "user2@truedat.net" }
32
- ]
29
+ { id: 2, name: "User 2", email: "user2@truedat.net" },
30
+ ],
33
31
  };
34
32
 
35
33
  it("should put a success action when a response is returned", () => {
@@ -1,6 +0,0 @@
1
- // Authentication
2
- export const REQUEST_LOGOUT = "REQUEST_LOGOUT";
3
- export const RECEIVE_LOGOUT = "RECEIVE_LOGOUT";
4
-
5
- export const REQUEST_TOKEN = "REQUEST_TOKEN";
6
- export const RECEIVE_TOKEN = "RECEIVE_TOKEN";