Authentification JWT sur le backend super-graph #8

Manually merged
tcornaut merged 12 commits from feature/super-graph-auth into develop 2020-06-22 21:28:41 +02:00
12 changed files with 156 additions and 14 deletions
Showing only changes of commit 369be98bd8 - Show all commits

View File

@ -0,0 +1,18 @@
/* fetchUser */
variables {
"email": ""
}
query fetchUser {
user(where: {email: {eq: $email}}) {
id
created_at
updated_at
email,
full_name
}
}

View File

@ -5499,6 +5499,11 @@
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
"dev": true "dev": true
}, },
"graphql-request": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-2.0.0.tgz",
"integrity": "sha512-Ww3Ax+G3l2d+mPT8w7HC9LfrKjutnCKtnDq7ZZp2ghVk5IQDjwAk3/arRF1ix17Ky15rm0hrSKVKxRhIVlSuoQ=="
},
"handle-thing": { "handle-thing": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",

View File

@ -54,6 +54,7 @@
"@types/qs": "^6.9.3", "@types/qs": "^6.9.3",
"bulma": "^0.7.2", "bulma": "^0.7.2",
"bulma-switch": "^2.0.0", "bulma-switch": "^2.0.0",
"graphql-request": "^2.0.0",
"jwt-decode": "^2.2.0", "jwt-decode": "^2.2.0",
"qs": "^6.9.4", "qs": "^6.9.4",
"react": "^16.12.0", "react": "^16.12.0",

View File

@ -1,6 +1,6 @@
import React from 'react'; import React, { useEffect } from 'react';
import { Page } from '../Page'; import { Page } from '../Page';
import { useSelector } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../../store/reducers/root'; import { RootState } from '../../store/reducers/root';
export function HomePage() { export function HomePage() {
@ -14,8 +14,8 @@ export function HomePage() {
<div className="column is-4 is-offset-4"> <div className="column is-4 is-offset-4">
<div className="box"> <div className="box">
{ {
currentUser ? currentUser && currentUser.full_name ?
<p>Bonjour <span className="has-text-weight-bold">{currentUser.email}</span> !</p> : <p>Bonjour <span className="has-text-weight-bold">{currentUser.full_name}</span> !</p> :
<p>Veuillez vous authentifier.</p> <p>Veuillez vous authentifier.</p>
} }
</div> </div>

View File

@ -7,7 +7,8 @@ export const Config = {
oauth2AuthorizeURL: get<string>("oauth2AuthorizeURL", "http://localhost:4444/oauth2/auth"), oauth2AuthorizeURL: get<string>("oauth2AuthorizeURL", "http://localhost:4444/oauth2/auth"),
oauth2TokenURL: get<string>("oauth2TokenURL", "http://localhost:4444/oauth2/token"), oauth2TokenURL: get<string>("oauth2TokenURL", "http://localhost:4444/oauth2/token"),
oauth2LogoutURL: get<string>("oauth2LogoutURL", "http://localhost:4444/oauth2/sessions/logout"), oauth2LogoutURL: get<string>("oauth2LogoutURL", "http://localhost:4444/oauth2/sessions/logout"),
oauth2PostLogoutRedirectURI: get<string>("oauth2PostLogoutRedirectURI", "http://localhost:8081") oauth2PostLogoutRedirectURI: get<string>("oauth2PostLogoutRedirectURI", "http://localhost:8081"),
graphQLEndpoint: get<string>("graphQLEndpoint", "http://localhost:8080/api/v1/graphql")
}; };
function get<T>(key: string, defaultValue: T):T { function get<T>(key: string, defaultValue: T):T {

View File

@ -0,0 +1,19 @@
import { Action } from "redux";
import { User } from "../../types/user";
export const FETCH_PROFILE_REQUEST = 'FETCH_PROFILE_REQUEST';
export const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
export const FETCH_PROFILE_FAILURE = 'FETCH_PROFILE_FAILURE';
export interface fetchProfileRequestAction extends Action {
}
export interface fetchProfileSuccessAction extends Action {
profile: User
}
export function fetchProfile(): fetchProfileRequestAction {
return { type: FETCH_PROFILE_REQUEST }
}

View File

@ -1,6 +1,7 @@
import { Action } from "redux"; import { Action } from "redux";
import { User } from "../../types/user"; import { User } from "../../types/user";
import { SET_CURRENT_USER, setCurrentUserAction, LOGOUT } from "../actions/auth"; import { SET_CURRENT_USER, setCurrentUserAction, LOGOUT } from "../actions/auth";
import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction } from "../actions/profile";
export interface AuthState { export interface AuthState {
isAuthenticated: boolean isAuthenticated: boolean
@ -18,6 +19,9 @@ export function authReducer(state = defaultState, action: Action): AuthState {
return handleSetCurrentUser(state, action as setCurrentUserAction); return handleSetCurrentUser(state, action as setCurrentUserAction);
case LOGOUT: case LOGOUT:
return handleLogout(state); return handleLogout(state);
case FETCH_PROFILE_SUCCESS:
return handleFetchProfileSuccess(state, action as fetchProfileSuccessAction);
} }
return state; return state;
} }
@ -38,4 +42,14 @@ function handleLogout(state: AuthState): AuthState {
isAuthenticated: false, isAuthenticated: false,
currentUser: null, currentUser: null,
}; };
} };
function handleFetchProfileSuccess(state: AuthState, { profile }: fetchProfileSuccessAction): AuthState {
return {
...state,
isAuthenticated: true,
currentUser: {
...profile,
}
};
};

View File

@ -0,0 +1,18 @@
import { all, put } from "redux-saga/effects";
import { getSavedAccessGrant } from "../../util/auth";
import { parseIdToken } from "../actions/auth";
export function* initRootSaga() {
yield all([
retrieveSessionSaga(),
]);
}
export function* retrieveSessionSaga() {
console.log("Checking session status...");
const accessGrant = getSavedAccessGrant();
if (!accessGrant) return;
yield put(parseIdToken(accessGrant.id_token));
}

View File

@ -1,10 +1,14 @@
import { all } from 'redux-saga/effects'; import { all } from 'redux-saga/effects';
import { failureRootSaga } from './failure'; import { failureRootSaga } from './failure';
import { authRootSaga } from './auth'; import { authRootSaga } from './auth';
import { initRootSaga } from './init';
import { usersRootSaga } from './users';
export function* rootSaga() { export function* rootSaga() {
yield all([ yield all([
initRootSaga(),
failureRootSaga(), failureRootSaga(),
authRootSaga(), authRootSaga(),
usersRootSaga(),
]); ]);
} }

View File

@ -0,0 +1,37 @@
import { DaddyClient } from "../../util/daddy";
import { Config } from "../../config";
import { getSavedAccessGrant } from "../../util/auth";
import { all, takeLatest, put, select } from "redux-saga/effects";
import { FETCH_PROFILE_REQUEST, fetchProfile, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS } from "../actions/profile";
import { SET_CURRENT_USER } from "../actions/auth";
import { RootState } from "../reducers/root";
import { User } from "../../types/user";
export function* usersRootSaga() {
yield all([
takeLatest(SET_CURRENT_USER, onCurrentUserChangeSaga),
takeLatest(FETCH_PROFILE_REQUEST, fetchProfileSaga),
]);
}
export function* onCurrentUserChangeSaga() {
yield put(fetchProfile());
}
export function* fetchProfileSaga() {
const grant = getSavedAccessGrant();
const client = new DaddyClient(Config.graphQLEndpoint, grant.id_token);
let profile: User;
try {
const currentUser: User = yield select((state: RootState) => state.auth.currentUser);
profile = yield client.fetchUser(currentUser.email).then(result => result.user);
} catch(err) {
yield put({ type: FETCH_PROFILE_FAILURE, err });
return;
}
yield put({type: FETCH_PROFILE_SUCCESS, profile });
}

View File

@ -1,3 +1,6 @@
export interface User { export interface User {
email: string email: string
full_name?: string
updated_at?: Date
created_at?: Date
} }

View File

@ -1,3 +1,6 @@
import { GraphQLClient } from 'graphql-request'
import { Config } from "../config";
export class UnauthorizedError extends Error { export class UnauthorizedError extends Error {
constructor(...args: any[]) { constructor(...args: any[]) {
super(...args) super(...args)
@ -7,16 +10,35 @@ export class UnauthorizedError extends Error {
export class DaddyClient { export class DaddyClient {
assertOk(res: any) { gql: GraphQLClient
if (!res.ok) return Promise.reject(new Error('Request failed'));
return res; constructor(endpoint: string, idToken: string) {
this.gql = new GraphQLClient(endpoint, {
headers: {
Authorization: `Bearer ${idToken}`,
mode: 'cors',
}
});
} }
assertAuthorization(res: any) { fetchUser(email: string) {
if (res.status === 401 || res.status === 404) return Promise.reject(new UnauthorizedError()); return this.gql.rawRequest(`
return res; query fetchUser {
user(where: {email: {eq: $email}}) {
id
created_at
updated_at
email,
full_name
}
}
`, { email })
.then(this.assertAuthorization)
}
assertAuthorization({ status, data }: any) {
if (status === 401) return Promise.reject(new UnauthorizedError());
return data;
} }
} }
export const daddy = new DaddyClient();