Authentification via OpenID Connect #6
|
@ -0,0 +1,4 @@
|
|||
[*.{ts,tsx,js,jsx}]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
5
Makefile
5
Makefile
|
@ -5,7 +5,7 @@ deps:
|
|||
cd frontend && npm install
|
||||
|
||||
up: build
|
||||
( cd frontend && npm run server ) & USER_ID=$(shell id -u) docker-compose up && wait
|
||||
( cd frontend && NODE_ENV=development npm run server ) & USER_ID=$(shell id -u) docker-compose up && wait
|
||||
|
||||
sg:
|
||||
docker-compose exec -u $(shell id -u) super-graph sh
|
||||
|
@ -18,3 +18,6 @@ down:
|
|||
|
||||
db-shell:
|
||||
docker-compose exec postgres psql -Usupergraph
|
||||
|
||||
hydra-shell:
|
||||
docker-compose exec hydra /bin/sh
|
|
@ -28,6 +28,9 @@ Les services suivants devraient être disponibles après démarrage de l'environ
|
|||
|Application React|HTTP (UI)|http://localhost:8081/|Page d'accueil de l'application React (serveur Webpack)|
|
||||
|Interface Web GraphQL|HTTP (UI)|http://localhost:8080/|Interface Web de développement de l'API GraphQL|
|
||||
|Serveur GraphQL|HTTP (GraphQL)|http://localhost:8080/api/v1/graphql|Point d'entrée de l'API GraphQL|
|
||||
|Serveur Hydra|HTTP (ReST)|http://localhost:4444|Point d'entrée pour l'API OAuth2 d'[Hydra](https://www.ory.sh/hydra/docs/)|
|
||||
|Serveur Hydra Passwordless|HTTP|http://localhost:3000|Point d'entrée pour la ["Login/Consent App"](https://www.ory.sh/hydra/docs/implementing-consent) [hydra-passwordless](https://forge.cadoles.com/wpetit/hydra-passwordless)|
|
||||
|Serveur FakeSMTP|HTTP|http://localhost:8082|Interface web du serveur [FakeSMTP](https://forge.cadoles.com/wpetit/fake-smtp)
|
||||
|Serveur PostgreSQL|TCP/IP (PostgreSQL)|`127.0.0.1:5432`|Port de connexion à la base de données PostgreSQL de développement|
|
||||
|
||||
#### Fichiers/répertoires notables
|
||||
|
@ -46,6 +49,10 @@ Les services suivants devraient être disponibles après démarrage de l'environ
|
|||
|`make down`|Stopper et supprimer l'environnement de développement.|
|
||||
|`make db-shell`|Ouvrir une console `psql` sur la base de données de développement.|
|
||||
|
||||
#### Ressources
|
||||
|
||||
- [Execute an Authorization Code Grant Flow with PKCE](https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce)
|
||||
|
||||
## Licence
|
||||
|
||||
AGPL-3.0
|
||||
|
|
|
@ -20,11 +20,71 @@ services:
|
|||
- postgres
|
||||
ports:
|
||||
- 8080:8080
|
||||
|
||||
postgres:
|
||||
image: postgres:12-alpine
|
||||
build:
|
||||
context: ./misc/containers/postgres
|
||||
args:
|
||||
- HTTP_PROXY=${HTTP_PROXY}
|
||||
- HTTPS_PROXY=${HTTPS_PROXY}
|
||||
- http_proxy=${http_proxy}
|
||||
- https_proxy=${https_proxy}
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=daddy
|
||||
- POSTGRES_USER=daddy
|
||||
- POSTGRES_DB=daddy
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
hydra:
|
||||
build:
|
||||
context: ./misc/containers/hydra
|
||||
environment:
|
||||
DSN: postgres://hydra:hydra@postgres:5432/hydra
|
||||
URLS_LOGIN: http://localhost:3000/login
|
||||
URLS_CONSENT: http://localhost:3000/consent
|
||||
URLS_LOGOUT: http://localhost:3000/logout
|
||||
SUPPORTED_SCOPES: email
|
||||
SUPPORTED_CLAIMS: email,email_verified
|
||||
SECRETS_SYSTEM: fAAya66yXNib52lbXpo16bxy1jD4NZrX
|
||||
HYDRA_ADMIN_URL: http://localhost:4445
|
||||
SERVE_PUBLIC_CORS_ENABLED: "true"
|
||||
SERVE_PUBLIC_CORS_ALLOWED_ORIGINS: http://localhost:8081
|
||||
ports:
|
||||
- 4444:4444
|
||||
command: hydra serve all --dangerous-force-http
|
||||
|
||||
hydra-passwordless:
|
||||
image: bornholm/hydra-passwordless
|
||||
ports:
|
||||
- 3000:3000
|
||||
environment:
|
||||
- HTTP_COOKIE_AUTHENTICATION_KEY=XNFEWQwYB9WiVSnkHoFnMtNDL6X88apR4DmDBwh7gVgdJ3LTdLRLwGZAALnVN2yg
|
||||
- HTTP_COOKIE_ENCRYPTION_KEY=xtHEd36Uo4DFeS2JgPPm94fPBfinY3xi
|
||||
- HTTP_TOKEN_AUTHENTICATION_KEY=sGToi4yiP5yWrZzKdKaDA3XNpkcg9CRAaycuhr5gy2XnPKzUS7N6wGEFhMq9WPuf
|
||||
- HTTP_TOKEN_ENCRYPTION_KEY=LAbuEWUeNDCLniRcyjiBCZ8ecgwN9Van
|
||||
- SMTP_HOST=smtp
|
||||
- SMTP_PORT=2525
|
||||
- SMTP_USE_START_TLS=false
|
||||
- SMTP_USER=
|
||||
- SMTP_PASSWORD=
|
||||
- SMTP_INSECURE_SKIP_VERIFY=true
|
||||
- HYDRA_BASE_URL=http://hydra:4445
|
||||
- HYDRA_FAKE_SSL_TERMINATION=false
|
||||
|
||||
smtp:
|
||||
image: bornholm/fake-smtp
|
||||
ports:
|
||||
- 8082:8080
|
||||
- 2525:2525
|
||||
environment:
|
||||
- FAKESMTP_SMTP_ADDRESS=:2525
|
||||
- FAKESMTP_SMTP_DEBUG=true
|
||||
- FAKESMTP_SMTP_USERNAME=
|
||||
- FAKESMTP_SMTP_PASSWORD=
|
||||
- FAKESMTP_SMTP_ALLOWINSECUREAUTH=true
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
volumes:
|
||||
postgres_data:
|
|
@ -1033,15 +1033,6 @@
|
|||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"@babel/runtime-corejs2": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.10.2.tgz",
|
||||
"integrity": "sha512-ZLwsFnNm3WpIARU1aLFtufjMHsmEnc8TjtrfAjmbgMbeoyR+LuQoyESoNdTfeDhL6IdY12SpeycXMgSgl8XGXA==",
|
||||
"requires": {
|
||||
"core-js": "^2.6.5",
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"@babel/template": {
|
||||
"version": "7.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.1.tgz",
|
||||
|
@ -1102,14 +1093,6 @@
|
|||
"integrity": "sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg==",
|
||||
"dev": true
|
||||
},
|
||||
"@lourenci/react-kanban": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@lourenci/react-kanban/-/react-kanban-0.15.0.tgz",
|
||||
"integrity": "sha512-/2XjB26iXcvpwDwlT3sz8/ptQ7QyTpMGlrPf1f02+V1Z4jdbVMo6Luz1sGlHe/TP68N8yz69/YT9qwqHZ6YYmQ==",
|
||||
"requires": {
|
||||
"react-beautiful-dnd": "^11.0.0"
|
||||
}
|
||||
},
|
||||
"@redux-saga/core": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.1.3.tgz",
|
||||
|
@ -1157,6 +1140,12 @@
|
|||
"resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.1.0.tgz",
|
||||
"integrity": "sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg=="
|
||||
},
|
||||
"@types/anymatch": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
|
||||
"integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/glob": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.2.tgz",
|
||||
|
@ -1207,6 +1196,11 @@
|
|||
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/qs": {
|
||||
"version": "6.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.3.tgz",
|
||||
"integrity": "sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA=="
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "16.9.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.36.tgz",
|
||||
|
@ -1259,12 +1253,82 @@
|
|||
"@types/react-router": "*"
|
||||
}
|
||||
},
|
||||
"@types/source-list-map": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
|
||||
"integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/tapable": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz",
|
||||
"integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/uglify-js": {
|
||||
"version": "3.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.9.2.tgz",
|
||||
"integrity": "sha512-d6dIfpPbF+8B7WiCi2ELY7m0w1joD8cRW4ms88Emdb2w062NeEpbNCeWwVCgzLRpVG+5e74VFSg4rgJ2xXjEiQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/uuid": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.4.tgz",
|
||||
"integrity": "sha512-WGZCqBZZ0mXN2RxvLHL6/7RCu+OWs28jgQMP04LWfpyJlQUMTR6YU9CNJAKDgbw+EV/u687INXuLUc7FuML/4g==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/webpack": {
|
||||
"version": "4.41.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.17.tgz",
|
||||
"integrity": "sha512-6FfeCidTSHozwKI67gIVQQ5Mp0g4X96c2IXxX75hYEQJwST/i6NyZexP//zzMOBb+wG9jJ7oO8fk9yObP2HWAw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/anymatch": "*",
|
||||
"@types/node": "*",
|
||||
"@types/tapable": "*",
|
||||
"@types/uglify-js": "*",
|
||||
"@types/webpack-sources": "*",
|
||||
"source-map": "^0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/webpack-sources": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-1.4.0.tgz",
|
||||
"integrity": "sha512-c88dKrpSle9BtTqR6ifdaxu1Lvjsl3C5OsfvuUbUwdXymshv1TkufUAXBajCCUM/f/TmnkZC/Esb03MinzSiXQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"@types/source-list-map": "*",
|
||||
"source-map": "^0.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
|
||||
|
@ -3208,6 +3272,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"clean-webpack-plugin": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz",
|
||||
"integrity": "sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/webpack": "^4.4.31",
|
||||
"del": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"cliui": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
|
||||
|
@ -3471,7 +3545,8 @@
|
|||
"core-js": {
|
||||
"version": "2.6.11",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
|
||||
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
|
||||
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==",
|
||||
"dev": true
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.6.5",
|
||||
|
@ -3591,14 +3666,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"css-box-model": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
|
||||
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
|
||||
"requires": {
|
||||
"tiny-invariant": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"css-color-keywords": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||
|
@ -5881,6 +5948,11 @@
|
|||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"jwt-decode": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
|
||||
"integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk="
|
||||
},
|
||||
"killable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
|
@ -5981,12 +6053,6 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||
},
|
||||
"lodash.union": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||
"integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=",
|
||||
"dev": true
|
||||
},
|
||||
"loglevel": {
|
||||
"version": "1.6.8",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz",
|
||||
|
@ -7348,10 +7414,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
||||
"dev": true
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
|
||||
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
|
||||
},
|
||||
"querystring": {
|
||||
"version": "0.2.0",
|
||||
|
@ -7371,11 +7436,6 @@
|
|||
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==",
|
||||
"dev": true
|
||||
},
|
||||
"raf-schd": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz",
|
||||
"integrity": "sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ=="
|
||||
},
|
||||
"randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
|
@ -7431,21 +7491,6 @@
|
|||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"react-beautiful-dnd": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-11.0.5.tgz",
|
||||
"integrity": "sha512-7llby9U+jIfkINcyxPHVWU0HFYzqxMemUYgGHsFsbx4fZo1n/pW6sYKYzhxGxR3Ap5HxqswcQkKUZX4uEUWhlw==",
|
||||
"requires": {
|
||||
"@babel/runtime-corejs2": "^7.4.5",
|
||||
"css-box-model": "^1.1.2",
|
||||
"memoize-one": "^5.0.4",
|
||||
"raf-schd": "^4.0.0",
|
||||
"react-redux": "^7.0.3",
|
||||
"redux": "^4.0.1",
|
||||
"tiny-invariant": "^1.0.4",
|
||||
"use-memo-one": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
|
||||
|
@ -7593,12 +7638,6 @@
|
|||
"source-map": "~0.5.0"
|
||||
}
|
||||
},
|
||||
"recursive-readdir-sync": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/recursive-readdir-sync/-/recursive-readdir-sync-1.0.6.tgz",
|
||||
"integrity": "sha1-Hb9tMvPFu4083pemxYjVR6nhPVY=",
|
||||
"dev": true
|
||||
},
|
||||
"redent": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
|
||||
|
@ -7800,6 +7839,14 @@
|
|||
"tough-cookie": "~2.5.0",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"qs": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"require-directory": {
|
||||
|
@ -9432,11 +9479,6 @@
|
|||
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
|
||||
"dev": true
|
||||
},
|
||||
"use-memo-one": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.1.tgz",
|
||||
"integrity": "sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ=="
|
||||
},
|
||||
"util": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
|
||||
|
@ -9485,7 +9527,8 @@
|
|||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"dev": true
|
||||
},
|
||||
"v8-compile-cache": {
|
||||
"version": "2.0.3",
|
||||
|
@ -9727,28 +9770,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"webpack-cleanup-plugin": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-cleanup-plugin/-/webpack-cleanup-plugin-0.5.1.tgz",
|
||||
"integrity": "sha1-3y1wa9dTZMBuZbBRGGMW1nTrlq8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash.union": "4.6.0",
|
||||
"minimatch": "3.0.3",
|
||||
"recursive-readdir-sync": "1.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimatch": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz",
|
||||
"integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"webpack-cli": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.11.tgz",
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/uuid": "^7.0.3",
|
||||
"babel-loader": "^8.0.6",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"css-loader": "^1.0.1",
|
||||
"extract-loader": "^3.1.0",
|
||||
"file-loader": "^2.0.0",
|
||||
|
@ -45,13 +46,15 @@
|
|||
"style-loader": "^0.23.1",
|
||||
"ts-loader": "^7.0.2",
|
||||
"webpack": "^4.25.0",
|
||||
"webpack-cleanup-plugin": "^0.5.1",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/qs": "^6.9.3",
|
||||
"bulma": "^0.7.2",
|
||||
"bulma-switch": "^2.0.0",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-redux": "^7.1.3",
|
||||
|
|
|
@ -1,29 +1,22 @@
|
|||
import React from 'react';
|
||||
import { HashRouter as Router, Route, Redirect, Switch } from "react-router-dom";
|
||||
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
|
||||
import { HomePage } from './HomePage/HomePage';
|
||||
import { store } from '../store/store';
|
||||
import { Provider } from 'react-redux';
|
||||
import { logout } from '../store/actions/logout';
|
||||
import { OAuth2Page } from './OAuth2Page/OAuth2Page';
|
||||
|
||||
export class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route path="/" exact component={HomePage} />
|
||||
<Route path="/logout" exact component={() => {
|
||||
this.logout();
|
||||
return <Redirect to="/" />;
|
||||
}} />
|
||||
<Route path="/oauth2/:action" exact component={OAuth2Page} />
|
||||
<Route component={() => <Redirect to="/" />} />
|
||||
</Switch>
|
||||
</Router>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
logout() {
|
||||
store.dispatch(logout());
|
||||
}
|
||||
}
|
|
@ -1,11 +1,27 @@
|
|||
import React from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../../store/reducers/root';
|
||||
|
||||
export function HomePage() {
|
||||
const currentUser = useSelector((state: RootState) => state.auth.currentUser);
|
||||
|
||||
return (
|
||||
<Page title="Daddy - Accueil">
|
||||
<div className="container is-fluid">
|
||||
|
||||
<section className="section">
|
||||
<div className="columns">
|
||||
<div className="column is-4 is-offset-4">
|
||||
<div className="box">
|
||||
{
|
||||
currentUser ?
|
||||
<p>Bonjour <span className="has-text-weight-bold">{currentUser.email}</span> !</p> :
|
||||
<p>Veuillez vous authentifier.</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
|
|
|
@ -1,36 +1,48 @@
|
|||
import React from 'react';
|
||||
import logo from '../resources/logo.svg';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../store/reducers/root';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export class Navbar extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<nav className="navbar" role="navigation" aria-label="main navigation">
|
||||
<div className="container is-fluid">
|
||||
<div className="navbar-brand">
|
||||
<a className="navbar-item" href="#/">
|
||||
<img src={logo} style={{marginRight:'5px',width:'28px',height:'28px'}} />
|
||||
<h1 className="is-size-4">Daddy</h1>
|
||||
</a>
|
||||
<a role="button" className="navbar-burger" aria-label="menu" aria-expanded="false">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="navbar-menu">
|
||||
<div className="navbar-end">
|
||||
<div className="navbar-item">
|
||||
<a className="button is-small" href="#/logout">
|
||||
export function Navbar() {
|
||||
const isAuthenticated = useSelector<RootState>(state => state.auth.isAuthenticated);
|
||||
|
||||
return (
|
||||
<nav className="navbar" role="navigation" aria-label="main navigation">
|
||||
<div className="container is-fluid">
|
||||
<div className="navbar-brand">
|
||||
<a className="navbar-item" href="#/">
|
||||
<img src={logo} style={{marginRight:'5px',width:'28px',height:'28px'}} />
|
||||
<h1 className="is-size-4">Daddy</h1>
|
||||
</a>
|
||||
<a role="button" className="navbar-burger" aria-label="menu" aria-expanded="false">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="navbar-menu">
|
||||
<div className="navbar-end">
|
||||
<div className="navbar-item">
|
||||
{
|
||||
isAuthenticated ?
|
||||
<Link className="button is-small" to="/oauth2/logout">
|
||||
<span className="icon">
|
||||
<i className="fas fa-sign-out-alt"></i>
|
||||
</span>
|
||||
<span>Se déconnecter</span>
|
||||
</a>
|
||||
</div>
|
||||
</Link> :
|
||||
<Link className="button is-small" to="/oauth2/login">
|
||||
<span className="icon">
|
||||
<i className="fas fa-sign-in-alt"></i>
|
||||
</span>
|
||||
<span>Se connecter</span>
|
||||
</Link>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { logout, login, handleOAuth2Callback } from '../../store/actions/auth';
|
||||
|
||||
export function OAuth2Page({ match, location, history }) {
|
||||
const dispatch = useDispatch();
|
||||
const { action } = match.params;
|
||||
|
||||
useEffect(() => {
|
||||
switch(action) {
|
||||
case 'logout':
|
||||
dispatch(logout());
|
||||
history.push("/");
|
||||
break;
|
||||
case 'login':
|
||||
dispatch(login());
|
||||
break;
|
||||
case 'callback':
|
||||
dispatch(handleOAuth2Callback(location.search));
|
||||
history.push("/");
|
||||
break;
|
||||
}
|
||||
}, [action]);
|
||||
|
||||
return (
|
||||
<Page title="Daddy - OAuth2">
|
||||
|
||||
</Page>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
export const Config = {
|
||||
// The OpenID Connect client_id
|
||||
oauth2ClientId: get<string>("oauth2ClientId", "daddy"),
|
||||
oauth2Scope: get<string>("oauth2Scope", "email email_verified openid offline_access"),
|
||||
oauth2RedirectURI: get<string>("oauth2RedirectURI", "http://localhost:8081/oauth2/callback"),
|
||||
oauth2Audience: get<string>("oauth2Audience", ""),
|
||||
oauth2AuthorizeURL: get<string>("oauth2AuthorizeURL", "http://localhost:4444/oauth2/auth"),
|
||||
oauth2TokenURL: get<string>("oauth2TokenURL", "http://localhost:4444/oauth2/token"),
|
||||
oauth2LogoutURL: get<string>("oauth2LogoutURL", "http://localhost:4444/oauth2/sessions/logout"),
|
||||
oauth2PostLogoutRedirectURI: get<string>("oauth2PostLogoutRedirectURI", "http://localhost:8081")
|
||||
};
|
||||
|
||||
function get<T>(key: string, defaultValue: T):T {
|
||||
const config = window['__CONFIG__'] || {};
|
||||
if (config && config.hasOwnProperty(key)) {
|
||||
return config[key] as T;
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
|
@ -5,16 +5,18 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Daddy</title>
|
||||
<% for (var css in htmlWebpackPlugin.files.css) { %>
|
||||
<link href="<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet">
|
||||
<link href="/<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet">
|
||||
<% } %>
|
||||
<% if (htmlWebpackPlugin.files.favicon) { %>
|
||||
<link rel="shortcut icon" href="<%= htmlWebpackPlugin.files.favicon%>">
|
||||
<link rel="shortcut icon" href="/<%= htmlWebpackPlugin.files.favicon%>">
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app" class="is-fullheight"></div>
|
||||
<script src="/config.js"></script>
|
||||
<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
|
||||
<script type="text/javascript" src="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
|
||||
<script type="text/javascript" src="/<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
|
||||
<% } %>
|
||||
</body>
|
||||
</html>
|
|
@ -2,6 +2,7 @@ import './sass/_all.scss';
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { App } from './components/App';
|
||||
import { Config } from './config';
|
||||
|
||||
import '@fortawesome/fontawesome-free/js/fontawesome'
|
||||
import '@fortawesome/fontawesome-free/js/solid'
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import { Action } from "redux";
|
||||
import { AccessGrant } from "../../util/auth";
|
||||
import { IdToken } from "../../types/idToken";
|
||||
|
||||
export const LOGOUT = "LOGOUT_REQUEST";
|
||||
|
||||
export function logout() {
|
||||
return { type: LOGOUT };
|
||||
};
|
||||
|
||||
export const LOGIN_REQUEST = "LOGIN_REQUEST";
|
||||
export const LOGIN_SUCCESS = "LOGIN_SUCCESS";
|
||||
export const LOGIN_FAILURE = "LOGIN_FAILURE";
|
||||
|
||||
export function login() {
|
||||
return { type: LOGIN_REQUEST };
|
||||
};
|
||||
|
||||
export const HANDLE_OAUTH2_CALLBACK_REQUEST = "HANDLE_OAUTH2_CALLBACK_REQUEST";
|
||||
export const HANDLE_OAUTH2_CALLBACK_SUCCESS = "HANDLE_OAUTH2_CALLBACK_SUCCESS";
|
||||
export const HANDLE_OAUTH2_CALLBACK_FAILURE = "HANDLE_OAUTH2_CALLBACK_FAILURE";
|
||||
|
||||
export interface handleOAuth2CallbackAction extends Action {
|
||||
search: string
|
||||
}
|
||||
|
||||
export function handleOAuth2Callback(search: string): handleOAuth2CallbackAction {
|
||||
return { type: HANDLE_OAUTH2_CALLBACK_REQUEST, search };
|
||||
};
|
||||
|
||||
export interface handleOAuth2CallbackSuccessAction extends Action {
|
||||
grant: AccessGrant
|
||||
}
|
||||
|
||||
export function handleOAuth2CallbackSuccess(grant: AccessGrant): handleOAuth2CallbackSuccessAction {
|
||||
return { type: HANDLE_OAUTH2_CALLBACK_SUCCESS, grant };
|
||||
};
|
||||
|
||||
export const PARSE_ID_TOKEN_REQUEST = "PARSE_ID_TOKEN_REQUEST";
|
||||
export const PARSE_ID_TOKEN_SUCCESS = "PARSE_ID_TOKEN_SUCCESS";
|
||||
export const PARSE_ID_TOKEN_FAILURE = "PARSE_ID_TOKEN_FAILURE";
|
||||
|
||||
export interface parseIdTokenAction extends Action {
|
||||
rawIdToken: string
|
||||
};
|
||||
|
||||
export function parseIdToken(rawIdToken: string): parseIdTokenAction {
|
||||
return { type: PARSE_ID_TOKEN_REQUEST, rawIdToken };
|
||||
};
|
||||
|
||||
|
||||
export interface parseIdTokenSuccessAction extends Action {
|
||||
idToken: IdToken
|
||||
}
|
||||
|
||||
export function parseIdTokenSuccess(idToken: IdToken): parseIdTokenSuccessAction {
|
||||
return { type: PARSE_ID_TOKEN_SUCCESS, idToken };
|
||||
};
|
||||
|
||||
|
||||
export const SET_CURRENT_USER = 'SET_CURRENT_USER';
|
||||
|
||||
export interface setCurrentUserAction extends Action {
|
||||
email: string
|
||||
}
|
||||
|
||||
export function setCurrentUser(email: string): setCurrentUserAction {
|
||||
return { type: SET_CURRENT_USER, email };
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
export const LOGOUT_REQUEST = "LOGOUT_REQUEST";
|
||||
export const LOGOUT_SUCCESS = "LOGOUT_SUCCESS";
|
||||
export const LOGOUT_FAILURE = "LOGOUT_FAILURE";
|
||||
|
||||
export function logout() {
|
||||
return { type: LOGOUT_REQUEST };
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
import { Action } from "redux";
|
||||
import { User } from "../../types/user";
|
||||
import { SET_CURRENT_USER, setCurrentUserAction, LOGOUT } from "../actions/auth";
|
||||
|
||||
export interface AuthState {
|
||||
isAuthenticated: boolean
|
||||
currentUser: User
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
isAuthenticated: false,
|
||||
currentUser: null,
|
||||
};
|
||||
|
||||
export function authReducer(state = defaultState, action: Action): AuthState {
|
||||
switch (action.type) {
|
||||
case SET_CURRENT_USER:
|
||||
return handleSetCurrentUser(state, action as setCurrentUserAction);
|
||||
case LOGOUT:
|
||||
return handleLogout(state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function handleSetCurrentUser(state: AuthState, { email }: setCurrentUserAction): AuthState {
|
||||
return {
|
||||
...state,
|
||||
isAuthenticated: true,
|
||||
currentUser: {
|
||||
email
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function handleLogout(state: AuthState): AuthState {
|
||||
return {
|
||||
...state,
|
||||
isAuthenticated: false,
|
||||
currentUser: null,
|
||||
};
|
||||
}
|
|
@ -1,8 +1,18 @@
|
|||
import { Action } from "redux";
|
||||
|
||||
export interface FlagsState {
|
||||
actions: { [actionName: string]: ActionState }
|
||||
}
|
||||
|
||||
export interface ActionState {
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
actions: {}
|
||||
};
|
||||
|
||||
export function flagsReducer(state = defaultState, action: any) {
|
||||
export function flagsReducer(state = defaultState, action: Action): FlagsState {
|
||||
const matches = (/^(.*)_((SUCCESS)|(FAILURE)|(REQUEST))$/).exec(action.type);
|
||||
|
||||
if(!matches) return state;
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import { flagsReducer } from './flags';
|
||||
import { flagsReducer, FlagsState } from './flags';
|
||||
import { authReducer, AuthState } from './auth';
|
||||
|
||||
export interface RootState {
|
||||
auth: AuthState,
|
||||
flags: FlagsState,
|
||||
}
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
flags: flagsReducer,
|
||||
auth: authReducer,
|
||||
});
|
|
@ -0,0 +1,98 @@
|
|||
import { put, takeLatest, all } from 'redux-saga/effects';
|
||||
import {
|
||||
LOGOUT, LOGIN_REQUEST,
|
||||
HANDLE_OAUTH2_CALLBACK_REQUEST, handleOAuth2CallbackAction,
|
||||
HANDLE_OAUTH2_CALLBACK_FAILURE, handleOAuth2CallbackSuccess,
|
||||
parseIdTokenAction, parseIdToken,
|
||||
PARSE_ID_TOKEN_REQUEST, PARSE_ID_TOKEN_FAILURE, parseIdTokenSuccess,
|
||||
setCurrentUser, LOGIN_FAILURE,
|
||||
} from '../actions/auth';
|
||||
import {
|
||||
createLoginSession, LoginSession,
|
||||
createAccessTokenRequest, saveAccessGrant,
|
||||
saveLoginSessionState, getSavedLoginSessionState,
|
||||
getLogoutURL, getSavedAccessGrant, clearAccessGrant
|
||||
} from '../../util/auth';
|
||||
import qs from 'qs';
|
||||
import { UnauthorizedError } from '../../util/daddy';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import { IdToken } from '../../types/idToken';
|
||||
|
||||
export function* authRootSaga() {
|
||||
yield all([
|
||||
takeLatest(LOGIN_REQUEST, loginSaga),
|
||||
takeLatest(LOGOUT, logoutSaga),
|
||||
takeLatest(HANDLE_OAUTH2_CALLBACK_REQUEST, handleOAuth2CallbackSaga),
|
||||
takeLatest(PARSE_ID_TOKEN_REQUEST, parseIDTokenSaga),
|
||||
]);
|
||||
}
|
||||
|
||||
export function* loginSaga() {
|
||||
try {
|
||||
const loginSession: LoginSession = yield createLoginSession();
|
||||
console.log('Code verifier is ', loginSession.verifier);
|
||||
console.log('State is ', loginSession.state);
|
||||
saveLoginSessionState(loginSession.verifier, loginSession.state);
|
||||
window.location.replace(loginSession.redirectUrl);
|
||||
} catch(err) {
|
||||
yield put({ type: LOGIN_FAILURE, err });
|
||||
}
|
||||
}
|
||||
|
||||
export function* logoutSaga() {
|
||||
const accessGrant = getSavedAccessGrant();
|
||||
const logoutURL = getLogoutURL(accessGrant.id_token);
|
||||
clearAccessGrant();
|
||||
window.location.replace(logoutURL);
|
||||
}
|
||||
|
||||
export function* handleOAuth2CallbackSaga({ search }: handleOAuth2CallbackAction) {
|
||||
const query = search.substring(1);
|
||||
const params = qs.parse(query);
|
||||
|
||||
const loginSession = getSavedLoginSessionState();
|
||||
|
||||
console.log('Stored state verifier is', loginSession.state);
|
||||
if (loginSession.state !== params.state) {
|
||||
yield put({ type: HANDLE_OAUTH2_CALLBACK_FAILURE, err: new Error("Invalid state") });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Stored code verifier is', loginSession.verifier);
|
||||
console.log('Authorization code is', params.code);
|
||||
|
||||
const req = createAccessTokenRequest(params.code as string, loginSession.verifier);
|
||||
|
||||
let grant;
|
||||
try {
|
||||
grant = yield fetch(req.url, { method: "POST", body: req.data })
|
||||
.then(res => {
|
||||
if (res.status === 401) return Promise.reject(new UnauthorizedError());
|
||||
return res;
|
||||
})
|
||||
.then(res => res.json());
|
||||
} catch(err) {
|
||||
yield put({ type: HANDLE_OAUTH2_CALLBACK_FAILURE, err });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Access grant is", grant);
|
||||
saveAccessGrant(grant);
|
||||
|
||||
yield put(handleOAuth2CallbackSuccess(grant));
|
||||
yield put(parseIdToken(grant.id_token));
|
||||
};
|
||||
|
||||
|
||||
export function* parseIDTokenSaga({ rawIdToken }: parseIdTokenAction) {
|
||||
let idToken: IdToken;
|
||||
try {
|
||||
idToken = jwtDecode(rawIdToken);
|
||||
} catch(err) {
|
||||
yield put({ type: PARSE_ID_TOKEN_FAILURE, err });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put(parseIdTokenSuccess(idToken));
|
||||
yield put(setCurrentUser(idToken.email));
|
||||
};
|
|
@ -1,9 +1,21 @@
|
|||
import { UnauthorizedError } from "../../util/daddy";
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { logout } from '../actions/logout';
|
||||
import { put, all, takeEvery } from 'redux-saga/effects';
|
||||
import { logout } from '../actions/auth';
|
||||
|
||||
export function* failureRootSaga() {
|
||||
yield all([
|
||||
takeEvery(patternFromRegExp(/^.*_FAILURE/), failuresSaga),
|
||||
]);
|
||||
}
|
||||
|
||||
export function* failuresSaga(action) {
|
||||
if (action.error instanceof UnauthorizedError) {
|
||||
yield put(logout());
|
||||
}
|
||||
}
|
||||
|
||||
export function patternFromRegExp(re: any) {
|
||||
return (action: any) => {
|
||||
return re.test(action.type);
|
||||
};
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import { call, put } from 'redux-saga/effects';
|
||||
import { LOGOUT_FAILURE, LOGOUT_SUCCESS } from '../actions/logout';
|
||||
|
||||
export function* logoutSaga() {
|
||||
try {
|
||||
yield call(fetch, '/logout', { mode: 'no-cors', credentials: 'include' });
|
||||
} catch(err) {
|
||||
yield put({ type: LOGOUT_FAILURE, error: err });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({ type: LOGOUT_SUCCESS });
|
||||
}
|
||||
|
||||
export function* logoutSuccessSaga() {
|
||||
window.location.reload();
|
||||
}
|
|
@ -1,18 +1,10 @@
|
|||
import { all, takeEvery, takeLatest } from 'redux-saga/effects';
|
||||
import { failuresSaga } from './failure';
|
||||
import { LOGOUT_REQUEST, LOGOUT_SUCCESS } from '../actions/logout';
|
||||
import { logoutSaga, logoutSuccessSaga } from './logout';
|
||||
import { all } from 'redux-saga/effects';
|
||||
import { failureRootSaga } from './failure';
|
||||
import { authRootSaga } from './auth';
|
||||
|
||||
export function* rootSaga() {
|
||||
yield all([
|
||||
takeEvery(patternFromRegExp(/^.*_FAILURE/), failuresSaga),
|
||||
takeLatest(LOGOUT_REQUEST, logoutSaga),
|
||||
takeLatest(LOGOUT_SUCCESS, logoutSuccessSaga)
|
||||
failureRootSaga(),
|
||||
authRootSaga(),
|
||||
]);
|
||||
}
|
||||
|
||||
export function patternFromRegExp(re: any) {
|
||||
return (action: any) => {
|
||||
return re.test(action.type);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface IdToken {
|
||||
email: string
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface User {
|
||||
email: string
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
import { Config } from '../config';
|
||||
|
||||
export interface LoginSession {
|
||||
state: string
|
||||
redirectUrl: string
|
||||
verifier: string
|
||||
}
|
||||
|
||||
export function generateRandomString() {
|
||||
var array = new Uint32Array(28);
|
||||
window.crypto.getRandomValues(array);
|
||||
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
|
||||
}
|
||||
|
||||
export function sha256(plain): PromiseLike<any> {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(plain);
|
||||
return window.crypto.subtle.digest('SHA-256', data);
|
||||
}
|
||||
|
||||
export function pkceChallengeFromVerifier(v): PromiseLike<string> {
|
||||
return sha256(v)
|
||||
.then(hashed => base64urlencode(hashed));
|
||||
}
|
||||
|
||||
export function base64urlencode(str) {
|
||||
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
|
||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
export function createLoginSession(): Promise<LoginSession> {
|
||||
// Based on https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce
|
||||
const state = generateRandomString();
|
||||
const verifier = generateRandomString();
|
||||
|
||||
return new Promise<LoginSession>((resolve, reject) => {
|
||||
try {
|
||||
pkceChallengeFromVerifier(verifier).then(challenge => {
|
||||
console.log('Code challenge is', challenge);
|
||||
|
||||
let redirectUrl=`${Config.oauth2AuthorizeURL}`;
|
||||
redirectUrl += `?audience=${encodeURIComponent(Config.oauth2Audience)}`;
|
||||
redirectUrl += `&scope=${encodeURIComponent(Config.oauth2Scope)}`;
|
||||
redirectUrl += `&response_type=code`;
|
||||
redirectUrl += `&client_id=${encodeURIComponent(Config.oauth2ClientId)}`
|
||||
redirectUrl += `&code_challenge=${encodeURIComponent(challenge)}`;
|
||||
redirectUrl += `&code_challenge_method=S256`
|
||||
redirectUrl += `&redirect_uri=${encodeURIComponent(Config.oauth2RedirectURI)}`;
|
||||
redirectUrl += `&state=${encodeURIComponent(state)}`;
|
||||
|
||||
return resolve({
|
||||
state,
|
||||
redirectUrl,
|
||||
verifier,
|
||||
});
|
||||
});
|
||||
} catch(err) {
|
||||
return reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export interface AccessTokenRequest {
|
||||
data: FormData,
|
||||
url: string
|
||||
}
|
||||
|
||||
export function createAccessTokenRequest(code: string, verifier: string): AccessTokenRequest {
|
||||
const data = new FormData();
|
||||
data.append('grant_type', 'authorization_code');
|
||||
data.append('client_id', Config.oauth2ClientId);
|
||||
data.append('code_verifier', verifier);
|
||||
data.append('code', code);
|
||||
data.append('redirect_uri', Config.oauth2RedirectURI);
|
||||
return {
|
||||
url: Config.oauth2TokenURL,
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
export function getLogoutURL(rawIdToken: string): string {
|
||||
let logoutURL = Config.oauth2LogoutURL;
|
||||
logoutURL += `?post_logout_redirect_uri=${encodeURIComponent(Config.oauth2PostLogoutRedirectURI)}`;
|
||||
logoutURL += `&id_token_hint=${encodeURIComponent(rawIdToken)}`;
|
||||
return logoutURL;
|
||||
}
|
||||
|
||||
export interface AccessGrant {
|
||||
access_token: string
|
||||
expires_in: number
|
||||
id_token: string
|
||||
refresh_token: string
|
||||
scope: string
|
||||
token_type: string
|
||||
}
|
||||
|
||||
export function saveLoginSessionState(verifier: string, state: string) {
|
||||
window.localStorage.setItem('login_verifier', verifier);
|
||||
window.localStorage.setItem('login_state', state);
|
||||
}
|
||||
|
||||
export function getSavedLoginSessionState(cleanup = true) {
|
||||
const loginSession = {
|
||||
verifier: window.localStorage.getItem('login_verifier'),
|
||||
state: window.localStorage.getItem('login_state')
|
||||
};
|
||||
if (cleanup) {
|
||||
window.localStorage.removeItem('login_verifier');
|
||||
window.localStorage.removeItem('login_state');
|
||||
}
|
||||
return loginSession;
|
||||
}
|
||||
|
||||
export function saveAccessGrant(grant: AccessGrant) {
|
||||
window.localStorage.setItem('access_grant', JSON.stringify(grant));
|
||||
}
|
||||
|
||||
export function getSavedAccessGrant(): AccessGrant {
|
||||
const raw = window.localStorage.getItem('access_grant');
|
||||
if (raw === "") return null;
|
||||
return JSON.parse(raw) as AccessGrant;
|
||||
}
|
||||
|
||||
export function clearAccessGrant() {
|
||||
window.localStorage.removeItem('access_grant');
|
||||
}
|
|
@ -3,7 +3,7 @@ const path = require('path');
|
|||
// Plugins
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
|
||||
const env = process.env;
|
||||
|
||||
|
@ -12,6 +12,7 @@ module.exports = {
|
|||
entry: './src/index.tsx',
|
||||
devtool: 'inline-source-map',
|
||||
output: {
|
||||
filename: '[name].[contenthash].js',
|
||||
path: path.join(__dirname, 'dist')
|
||||
},
|
||||
resolve: {
|
||||
|
@ -20,7 +21,9 @@ module.exports = {
|
|||
devServer: {
|
||||
contentBase: path.join(__dirname, 'dist'),
|
||||
compress: true,
|
||||
port: 8081
|
||||
port: 8081,
|
||||
historyApiFallback: true,
|
||||
writeToDisk: true,
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
|
@ -48,7 +51,7 @@ module.exports = {
|
|||
use: [{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
name: '[name].[contenthash].[ext]',
|
||||
outputPath: '/resources/'
|
||||
}
|
||||
}]
|
||||
|
@ -59,17 +62,15 @@ module.exports = {
|
|||
}]
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "css/[name].css",
|
||||
filename: "css/[name].[contenthash].css",
|
||||
chunkFilename: "css/[id].css"
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/index.html',
|
||||
inject: false,
|
||||
favicon: "./src/resources/favicon.png"
|
||||
favicon: "./src/resources/favicon.png",
|
||||
}),
|
||||
new WebpackCleanupPlugin({
|
||||
exclude: ['resources/logo.svg']
|
||||
})
|
||||
]
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
FROM oryd/hydra:v1.4.2-alpine
|
||||
|
||||
USER root
|
||||
|
||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint
|
||||
RUN chmod a+x /usr/local/bin/docker-entrypoint
|
||||
|
||||
COPY first-run.sh /usr/local/bin/docker-first-run
|
||||
RUN chmod a+x /usr/local/bin/docker-first-run
|
||||
|
||||
COPY hydra-init.d /hydra-init.d
|
||||
|
||||
RUN mkdir -p /home/ory && chown -R ory: /home/ory
|
||||
USER ory
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint"]
|
||||
|
||||
CMD ["hydra", "serve", "all"]
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -xeo pipefail
|
||||
|
||||
LIFECYCLEFLAGS_DIR="$HOME/.container-lifecycle"
|
||||
|
||||
mkdir -p "$LIFECYCLEFLAGS_DIR"
|
||||
|
||||
if [ ! -f "$LIFECYCLEFLAGS_DIR/first-run" ]; then
|
||||
/usr/local/bin/docker-first-run
|
||||
touch "$LIFECYCLEFLAGS_DIR/first-run"
|
||||
fi
|
||||
|
||||
exec "$@"
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
|
||||
hydra migrate sql -e -y
|
||||
|
||||
hydra serve all --dangerous-force-http &
|
||||
HYDRA_PID=$!
|
||||
run-parts --exit-on-error /hydra-init.d
|
||||
kill $HYDRA_PID
|
|
@ -0,0 +1,9 @@
|
|||
#!/bin/sh
|
||||
|
||||
hydra clients create \
|
||||
--id daddy \
|
||||
-n Daddy \
|
||||
-a email,email_verified,offline_access,openid \
|
||||
--token-endpoint-auth-method none \
|
||||
--post-logout-callbacks http://localhost:8081 \
|
||||
-c http://localhost:8081/oauth2/callback
|
|
@ -0,0 +1,3 @@
|
|||
FROM postgres:12-alpine
|
||||
|
||||
COPY ./initdb.d /docker-entrypoint-initdb.d
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||
CREATE USER hydra WITH ENCRYPTED PASSWORD 'hydra';
|
||||
CREATE DATABASE hydra;
|
||||
GRANT ALL PRIVILEGES ON DATABASE hydra TO hydra;
|
||||
EOSQL
|
||||
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||
CREATE USER daddy WITH ENCRYPTED PASSWORD 'daddy';
|
||||
CREATE DATABASE daddy;
|
||||
GRANT ALL PRIVILEGES ON DATABASE daddy TO daddy;
|
||||
EOSQL
|
Loading…
Reference in New Issue