Merge pull request #270 from authorizerdev/fix/oauth-provider

fix(server): authorizer openid flow
This commit is contained in:
Lakhan Samani 2022-11-16 12:27:30 +05:30 committed by GitHub
commit f9d2130c65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1125 additions and 739 deletions

View File

@ -2,16 +2,16 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
logLevel: logLevel:
description: "Log level" description: 'Log level'
required: true required: true
default: "warning" default: 'warning'
type: choice type: choice
options: options:
- info - info
- warning - warning
- debug - debug
tags: tags:
description: "Tags" description: 'Tags'
required: false required: false
type: boolean type: boolean
release: release:
@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: "16" node-version: '16'
- # Add support for more platforms with QEMU (optional) - # Add support for more platforms with QEMU (optional)
# https://github.com/docker/setup-qemu-action # https://github.com/docker/setup-qemu-action
name: Set up QEMU name: Set up QEMU
@ -36,7 +36,7 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: "^1.17.3" go-version: '^1.19.1'
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt-get install build-essential wget zip libc6-dev-arm64-cross && \ sudo apt-get install build-essential wget zip libc6-dev-arm64-cross && \

View File

@ -2,5 +2,5 @@
"tabWidth": 2, "tabWidth": 2,
"singleQuote": true, "singleQuote": true,
"trailingComma": "all", "trailingComma": "all",
"useTabs": false "useTabs": true
} }

30
app/package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^1.1.2", "@authorizerdev/authorizer-react": "^1.1.3-beta.1",
"@types/react": "^17.0.15", "@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17", "esbuild": "^0.12.17",
@ -27,9 +27,9 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-js": { "node_modules/@authorizerdev/authorizer-js": {
"version": "1.1.0", "version": "1.1.2-beta.1",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.1.2-beta.1.tgz",
"integrity": "sha512-MdEw1SjhIm7pXq20AscHSbnAta2PC3w7GNBY52/OzmlBXUGH3ooUQX/aszbYOse3FlhapcrGrRvg4sNM7faGAg==", "integrity": "sha512-u+O2iB3tqF1HtdJ6LfBXL9iMycqlCCL3othBQkqitGP1ldhASWLJ2pcXZAcHgyoeczKdj2XKZKdIcWB3GYR0IQ==",
"dependencies": { "dependencies": {
"cross-fetch": "^3.1.5" "cross-fetch": "^3.1.5"
}, },
@ -38,11 +38,11 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-react": { "node_modules/@authorizerdev/authorizer-react": {
"version": "1.1.2", "version": "1.1.3-beta.1",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.3-beta.1.tgz",
"integrity": "sha512-uBmuKnOVX8gp8CEUuGJuz04ep+8qMEzJXWd5leEGKYMIgolHpu/lOinnMUXhjh8YL3pA4+EhvB+hQXxUX+rRHQ==", "integrity": "sha512-+ZsOBp6XjZVnDyeJCXgaqZ8xzFO7ygpHB6v2cblCKIA3wX5pg/Dsg1oumHGrSHIEK8No/GOtCjSx4Rv6/CweBQ==",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": "^1.1.0", "@authorizerdev/authorizer-js": "^1.1.2-beta.1",
"final-form": "^4.20.2", "final-form": "^4.20.2",
"react-final-form": "^6.5.3", "react-final-form": "^6.5.3",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"
@ -876,19 +876,19 @@
}, },
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": { "@authorizerdev/authorizer-js": {
"version": "1.1.0", "version": "1.1.2-beta.1",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.1.2-beta.1.tgz",
"integrity": "sha512-MdEw1SjhIm7pXq20AscHSbnAta2PC3w7GNBY52/OzmlBXUGH3ooUQX/aszbYOse3FlhapcrGrRvg4sNM7faGAg==", "integrity": "sha512-u+O2iB3tqF1HtdJ6LfBXL9iMycqlCCL3othBQkqitGP1ldhASWLJ2pcXZAcHgyoeczKdj2XKZKdIcWB3GYR0IQ==",
"requires": { "requires": {
"cross-fetch": "^3.1.5" "cross-fetch": "^3.1.5"
} }
}, },
"@authorizerdev/authorizer-react": { "@authorizerdev/authorizer-react": {
"version": "1.1.2", "version": "1.1.3-beta.1",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.3-beta.1.tgz",
"integrity": "sha512-uBmuKnOVX8gp8CEUuGJuz04ep+8qMEzJXWd5leEGKYMIgolHpu/lOinnMUXhjh8YL3pA4+EhvB+hQXxUX+rRHQ==", "integrity": "sha512-+ZsOBp6XjZVnDyeJCXgaqZ8xzFO7ygpHB6v2cblCKIA3wX5pg/Dsg1oumHGrSHIEK8No/GOtCjSx4Rv6/CweBQ==",
"requires": { "requires": {
"@authorizerdev/authorizer-js": "^1.1.0", "@authorizerdev/authorizer-js": "^1.1.2-beta.1",
"final-form": "^4.20.2", "final-form": "^4.20.2",
"react-final-form": "^6.5.3", "react-final-form": "^6.5.3",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"

View File

@ -6,13 +6,13 @@
"scripts": { "scripts": {
"build": "rm -rf build && NODE_ENV=production node ./esbuild.config.js", "build": "rm -rf build && NODE_ENV=production node ./esbuild.config.js",
"start": "NODE_ENV=development node ./esbuild.config.js", "start": "NODE_ENV=development node ./esbuild.config.js",
"format": "prettier --write --use-tabs 'src/**/*.(ts|tsx|js|jsx)'" "format": "prettier --write 'src/**/*.(ts|tsx|js|jsx)'"
}, },
"keywords": [], "keywords": [],
"author": "Lakhan Samani", "author": "Lakhan Samani",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^1.1.2", "@authorizerdev/authorizer-react": "^1.1.3",
"@types/react": "^17.0.15", "@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17", "esbuild": "^0.12.17",

View File

@ -38,6 +38,8 @@ export default function Root({
const scope = searchParams.get('scope') const scope = searchParams.get('scope')
? searchParams.get('scope')?.toString().split(' ') ? searchParams.get('scope')?.toString().split(' ')
: ['openid', 'profile', 'email']; : ['openid', 'profile', 'email'];
const code = searchParams.get('code') || '';
const nonce = searchParams.get('nonce') || '';
const urlProps: Record<string, any> = { const urlProps: Record<string, any> = {
state, state,
@ -58,9 +60,19 @@ export default function Root({
if (token) { if (token) {
let redirectURL = config.redirectURL || '/app'; let redirectURL = config.redirectURL || '/app';
let params = `access_token=${token.access_token}&id_token=${token.id_token}&expires_in=${token.expires_in}&state=${globalState.state}`; let params = `access_token=${token.access_token}&id_token=${token.id_token}&expires_in=${token.expires_in}&state=${globalState.state}`;
if (code !== '') {
params += `&code=${code}`;
}
if (nonce !== '') {
params += `&nonce=${nonce}`;
}
if (token.refresh_token) { if (token.refresh_token) {
params += `&refresh_token=${token.refresh_token}`; params += `&refresh_token=${token.refresh_token}`;
} }
const url = new URL(redirectURL); const url = new URL(redirectURL);
if (redirectURL.includes('?')) { if (redirectURL.includes('?')) {
redirectURL = `${redirectURL}&${params}`; redirectURL = `${redirectURL}&${params}`;
@ -74,7 +86,7 @@ export default function Root({
} }
} }
return () => {}; return () => {};
}, [token]); }, [token, config]);
if (loading) { if (loading) {
return <h1>Loading...</h1>; return <h1>Loading...</h1>;
@ -100,7 +112,7 @@ export default function Root({
<Route path="/app" exact> <Route path="/app" exact>
<Login urlProps={urlProps} /> <Login urlProps={urlProps} />
</Route> </Route>
<Route path="/app/signup" exact> <Route path="/app/signup">
<SignUp urlProps={urlProps} /> <SignUp urlProps={urlProps} />
</Route> </Route>
<Route path="/app/reset-password"> <Route path="/app/reset-password">

View File

@ -2,5 +2,5 @@
"tabWidth": 2, "tabWidth": 2,
"singleQuote": true, "singleQuote": true,
"trailingComma": "all", "trailingComma": "all",
"useTabs": false "useTabs": true
} }

View File

@ -0,0 +1,19 @@
package constants
const (
// - query: for Authorization Code grant. 302 Found triggers redirect.
ResponseModeQuery = "query"
// - fragment: for Implicit grant. 302 Found triggers redirect.
ResponseModeFragment = "fragment"
// - form_post: 200 OK with response parameters embedded in an HTML form as hidden parameters.
ResponseModeFormPost = "form_post"
// - web_message: For Silent Authentication. Uses HTML5 web messaging.
ResponseModeWebMessage = "web_message"
// For the Authorization Code grant, use response_type=code to include the authorization code.
ResponseTypeCode = "code"
// For the Implicit grant, use response_type=token to include an access token.
ResponseTypeToken = "token"
// For the Implicit grant of id_token, use response_type=id_token to include an identifier token.
ResponseTypeIDToken = "id_token"
)

View File

@ -7,9 +7,8 @@ require (
github.com/arangodb/go-driver v1.2.1 github.com/arangodb/go-driver v1.2.1
github.com/aws/aws-sdk-go v1.44.109 github.com/aws/aws-sdk-go v1.44.109
github.com/coreos/go-oidc/v3 v3.1.0 github.com/coreos/go-oidc/v3 v3.1.0
github.com/denisenkom/go-mssqldb v0.11.0 // indirect
github.com/gin-gonic/gin v1.8.1 github.com/gin-gonic/gin v1.8.1
github.com/glebarez/sqlite v1.5.0 // indirect github.com/glebarez/sqlite v1.5.0
github.com/go-playground/validator/v10 v10.11.1 // indirect github.com/go-playground/validator/v10 v10.11.1 // indirect
github.com/go-redis/redis/v8 v8.11.0 github.com/go-redis/redis/v8 v8.11.0
github.com/goccy/go-json v0.9.11 // indirect github.com/goccy/go-json v0.9.11 // indirect
@ -19,7 +18,6 @@ require (
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/guregu/dynamo v1.16.0 github.com/guregu/dynamo v1.16.0
github.com/joho/godotenv v1.3.0 github.com/joho/godotenv v1.3.0
github.com/mitchellh/gox v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f

View File

@ -85,8 +85,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
github.com/denisenkom/go-mssqldb v0.11.0 h1:9rHa233rhdOyrz2GcP9NM+gi2psgJZ4GWDpL/7ND8HI=
github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
@ -141,7 +139,6 @@ github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@ -213,8 +210,6 @@ github.com/guregu/dynamo v1.16.0 h1:gmI8oi1VHwYQtq7+RPBeOiSssVLgxH/Az2t+NtDtL2c=
github.com/guregu/dynamo v1.16.0/go.mod h1:W2Gqcf3MtkrS+Q6fHPGAmRtT0Dyq+TGrqfqrUC9+R/c= github.com/guregu/dynamo v1.16.0/go.mod h1:W2Gqcf3MtkrS+Q6fHPGAmRtT0Dyq+TGrqfqrUC9+R/c=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8=
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
@ -232,8 +227,6 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8=
github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
@ -252,8 +245,6 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns=
github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
@ -262,28 +253,20 @@ github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01C
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.9.0 h1:/SH1RxEtltvJgsDqp3TbiTFApD3mey3iygpuEGeuBXk=
github.com/jackc/pgtype v1.9.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.14.0 h1:TgdrmgnM7VY72EuSQzBbBd4JA1RLqJolrw9nQVZABVc=
github.com/jackc/pgx/v4 v4.14.0/go.mod h1:jT3ibf/A0ZVCp89rtCIN0zCJxcE74ypROmHEZYsG/j8=
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.3 h1:PlHq1bSCSZL9K0wUhbm2pGLoTWs2GwVhsP6emvGV/ZI=
github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
@ -332,16 +315,9 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
github.com/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI=
github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4=
github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA= github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA=
github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -457,7 +433,6 @@ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -472,8 +447,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0= golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -814,26 +787,13 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.2.1 h1:h+3f1l9Ng2C072Y2tIiLgPpWN78r1KXL7bHJ0nTjlhU=
gorm.io/driver/mysql v1.2.1/go.mod h1:qsiz+XcAyMrS6QY+X3M9R6b/lKM1imKmcuK9kac5LTo=
gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k= gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
gorm.io/driver/postgres v1.2.3 h1:f4t0TmNMy9gh3TU2PX+EppoA6YsgFnyq8Ojtddb42To=
gorm.io/driver/postgres v1.2.3/go.mod h1:pJV6RgYQPG47aM1f0QeOzFH9HxQc8JcmAgjRCgS0wjs=
gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc= gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc=
gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg= gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg=
gorm.io/driver/sqlite v1.2.6 h1:SStaH/b+280M7C8vXeZLz/zo9cLQmIGwwj3cSj7p6l4=
gorm.io/driver/sqlite v1.2.6/go.mod h1:gyoX0vHiiwi0g49tv+x2E7l8ksauLK0U/gShcdUsjWY=
gorm.io/driver/sqlserver v1.2.1 h1:KhGOjvPX7JZ5hPyQICTJfMuTz88zgJ2lk9bWiHVNHd8=
gorm.io/driver/sqlserver v1.2.1/go.mod h1:nixq0OB3iLXZDiPv6JSOjWuPgpyaRpOIIevYtA4Ulb4=
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig=
gorm.io/gorm v1.22.2/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
gorm.io/gorm v1.22.4 h1:8aPcyEJhY0MAt8aY6Dc524Pn+pO29K+ydu+e/cXSpQM=
gorm.io/gorm v1.22.4/go.mod h1:1aeVC+pe9ZmvKZban/gW4QPra7PRoTEssyc922qCAkk=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.1 h1:CgvzRniUdG67hBAzsxDGOAuq4Te1osVMYsa1eQbd4fs= gorm.io/gorm v1.24.1 h1:CgvzRniUdG67hBAzsxDGOAuq4Te1osVMYsa1eQbd4fs=

View File

@ -2250,6 +2250,10 @@ input SignUpInput {
scope: [String!] scope: [String!]
redirect_uri: String redirect_uri: String
is_multi_factor_auth_enabled: Boolean is_multi_factor_auth_enabled: Boolean
# state is used for authorization code grant flow
# it is used to get code for an on-going auth process during login
# and use that code for setting ` + "`" + `c_hash` + "`" + ` in id_token
state: String
} }
input LoginInput { input LoginInput {
@ -2257,15 +2261,27 @@ input LoginInput {
password: String! password: String!
roles: [String!] roles: [String!]
scope: [String!] scope: [String!]
# state is used for authorization code grant flow
# it is used to get code for an on-going auth process during login
# and use that code for setting ` + "`" + `c_hash` + "`" + ` in id_token
state: String
} }
input VerifyEmailInput { input VerifyEmailInput {
token: String! token: String!
# state is used for authorization code grant flow
# it is used to get code for an on-going auth process during login
# and use that code for setting ` + "`" + `c_hash` + "`" + ` in id_token
state: String
} }
input ResendVerifyEmailInput { input ResendVerifyEmailInput {
email: String! email: String!
identifier: String! identifier: String!
# state is used for authorization code grant flow
# it is used to get code for an on-going auth process during login
# and use that code for setting ` + "`" + `c_hash` + "`" + ` in id_token
state: String
} }
input UpdateProfileInput { input UpdateProfileInput {
@ -2413,10 +2429,18 @@ input DeleteEmailTemplateRequest {
input VerifyOTPRequest { input VerifyOTPRequest {
email: String! email: String!
otp: String! otp: String!
# state is used for authorization code grant flow
# it is used to get code for an on-going auth process during login
# and use that code for setting ` + "`" + `c_hash` + "`" + ` in id_token
state: String
} }
input ResendOTPRequest { input ResendOTPRequest {
email: String! email: String!
# state is used for authorization code grant flow
# it is used to get code for an on-going auth process during login
# and use that code for setting ` + "`" + `c_hash` + "`" + ` in id_token
state: String
} }
type Mutation { type Mutation {
@ -14455,7 +14479,7 @@ func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj in
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"email", "password", "roles", "scope"} fieldsInOrder := [...]string{"email", "password", "roles", "scope", "state"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@ -14494,6 +14518,14 @@ func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj in
if err != nil { if err != nil {
return it, err return it, err
} }
case "state":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
it.State, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
} }
} }
@ -14659,7 +14691,7 @@ func (ec *executionContext) unmarshalInputResendOTPRequest(ctx context.Context,
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"email"} fieldsInOrder := [...]string{"email", "state"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@ -14674,6 +14706,14 @@ func (ec *executionContext) unmarshalInputResendOTPRequest(ctx context.Context,
if err != nil { if err != nil {
return it, err return it, err
} }
case "state":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
it.State, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
} }
} }
@ -14687,7 +14727,7 @@ func (ec *executionContext) unmarshalInputResendVerifyEmailInput(ctx context.Con
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"email", "identifier"} fieldsInOrder := [...]string{"email", "identifier", "state"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@ -14710,6 +14750,14 @@ func (ec *executionContext) unmarshalInputResendVerifyEmailInput(ctx context.Con
if err != nil { if err != nil {
return it, err return it, err
} }
case "state":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
it.State, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
} }
} }
@ -14803,7 +14851,7 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj i
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"email", "given_name", "family_name", "middle_name", "nickname", "gender", "birthdate", "phone_number", "picture", "password", "confirm_password", "roles", "scope", "redirect_uri", "is_multi_factor_auth_enabled"} fieldsInOrder := [...]string{"email", "given_name", "family_name", "middle_name", "nickname", "gender", "birthdate", "phone_number", "picture", "password", "confirm_password", "roles", "scope", "redirect_uri", "is_multi_factor_auth_enabled", "state"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@ -14930,6 +14978,14 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj i
if err != nil { if err != nil {
return it, err return it, err
} }
case "state":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
it.State, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
} }
} }
@ -15815,7 +15871,7 @@ func (ec *executionContext) unmarshalInputVerifyEmailInput(ctx context.Context,
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"token"} fieldsInOrder := [...]string{"token", "state"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@ -15830,6 +15886,14 @@ func (ec *executionContext) unmarshalInputVerifyEmailInput(ctx context.Context,
if err != nil { if err != nil {
return it, err return it, err
} }
case "state":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
it.State, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
} }
} }
@ -15843,7 +15907,7 @@ func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context,
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"email", "otp"} fieldsInOrder := [...]string{"email", "otp", "state"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@ -15866,6 +15930,14 @@ func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context,
if err != nil { if err != nil {
return it, err return it, err
} }
case "state":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
it.State, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
} }
} }

View File

@ -151,6 +151,7 @@ type LoginInput struct {
Password string `json:"password"` Password string `json:"password"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
Scope []string `json:"scope"` Scope []string `json:"scope"`
State *string `json:"state"`
} }
type MagicLinkLoginInput struct { type MagicLinkLoginInput struct {
@ -200,11 +201,13 @@ type PaginationInput struct {
type ResendOTPRequest struct { type ResendOTPRequest struct {
Email string `json:"email"` Email string `json:"email"`
State *string `json:"state"`
} }
type ResendVerifyEmailInput struct { type ResendVerifyEmailInput struct {
Email string `json:"email"` Email string `json:"email"`
Identifier string `json:"identifier"` Identifier string `json:"identifier"`
State *string `json:"state"`
} }
type ResetPasswordInput struct { type ResetPasswordInput struct {
@ -238,6 +241,7 @@ type SignUpInput struct {
Scope []string `json:"scope"` Scope []string `json:"scope"`
RedirectURI *string `json:"redirect_uri"` RedirectURI *string `json:"redirect_uri"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"` IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"`
State *string `json:"state"`
} }
type TestEndpointRequest struct { type TestEndpointRequest struct {
@ -409,11 +413,13 @@ type VerificationRequests struct {
type VerifyEmailInput struct { type VerifyEmailInput struct {
Token string `json:"token"` Token string `json:"token"`
State *string `json:"state"`
} }
type VerifyOTPRequest struct { type VerifyOTPRequest struct {
Email string `json:"email"` Email string `json:"email"`
Otp string `json:"otp"` Otp string `json:"otp"`
State *string `json:"state"`
} }
type Webhook struct { type Webhook struct {

View File

@ -285,6 +285,10 @@ input SignUpInput {
scope: [String!] scope: [String!]
redirect_uri: String redirect_uri: String
is_multi_factor_auth_enabled: Boolean is_multi_factor_auth_enabled: Boolean
# state is used for authorization code grant flow
# it is used to get code for an on-going auth process during login
# and use that code for setting `c_hash` in id_token
state: String
} }
input LoginInput { input LoginInput {
@ -292,15 +296,27 @@ input LoginInput {
password: String! password: String!
roles: [String!] roles: [String!]
scope: [String!] scope: [String!]
# state is used for authorization code grant flow
# it is used to get code for an on-going auth process during login
# and use that code for setting `c_hash` in id_token
state: String
} }
input VerifyEmailInput { input VerifyEmailInput {
token: String! token: String!
# state is used for authorization code grant flow
# it is used to get code for an on-going auth process during login
# and use that code for setting `c_hash` in id_token
state: String
} }
input ResendVerifyEmailInput { input ResendVerifyEmailInput {
email: String! email: String!
identifier: String! identifier: String!
# state is used for authorization code grant flow
# it is used to get code for an on-going auth process during login
# and use that code for setting `c_hash` in id_token
state: String
} }
input UpdateProfileInput { input UpdateProfileInput {
@ -430,6 +446,8 @@ input AddEmailTemplateRequest {
event_name: String! event_name: String!
subject: String! subject: String!
template: String! template: String!
# Design value is set when editor is used
# If raw HTML is used design value is set to null
design: String design: String
} }
@ -438,6 +456,8 @@ input UpdateEmailTemplateRequest {
event_name: String event_name: String
template: String template: String
subject: String subject: String
# Design value is set when editor is used
# If raw HTML is used design value is set to null
design: String design: String
} }
@ -448,10 +468,18 @@ input DeleteEmailTemplateRequest {
input VerifyOTPRequest { input VerifyOTPRequest {
email: String! email: String!
otp: String! otp: String!
# state is used for authorization code grant flow
# it is used to get code for an on-going auth process during login
# and use that code for setting `c_hash` in id_token
state: String
} }
input ResendOTPRequest { input ResendOTPRequest {
email: String! email: String!
# state is used for authorization code grant flow
# it is used to get code for an on-going auth process during login
# and use that code for setting `c_hash` in id_token
state: String
} }
type Mutation { type Mutation {

View File

@ -1,10 +1,41 @@
package handlers package handlers
/**
LOGIC TO REMEMBER THE AUTHORIZE FLOW
jargons
`at_hash` -> access_token_hash
`c_hash` -> code_hash
# ResponseType: Code
with /authorize request
- set state [state, code@@challenge]
- add &code to login redirect url
login resolver has optional param state
-if state found in store, split with @@
- if len > 1 -> response type is code and has code + challenge
- set `nonce, code` for createAuthToken request so that `c_hash` can be generated
- do not add `nonce` to id_token in code flow, instead set `c_hash` and `at_hash`
# ResponseType: token / id_token
with /authorize request
- set state [state, nonce]
- add &nonce to login redirect url
login resolver has optional param state
- if state found in store, split with @@
- if len < 1 -> response type is token / id_token and value is nonce
- send received nonce for createAuthToken with empty code value
- set `nonce` and `at_hash` in `id_token`
**/
import ( import (
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
@ -17,6 +48,15 @@ import (
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
) )
// Check the flow for generating and verifying codes: https://developer.okta.com/blog/2019/08/22/okta-authjs-pkce#:~:text=PKCE%20works%20by%20having%20the,is%20called%20the%20Code%20Challenge.
// Check following docs for understanding request / response params for various types of requests: https://auth0.com/docs/authenticate/login/oidc-conformant-authentication/oidc-adoption-auth-code-flow
const (
authorizeWebMessageTemplate = "authorize_web_message.tmpl"
authorizeFormPostTemplate = "authorize_form_post.tmpl"
)
// AuthorizeHandler is the handler for the /authorize route // AuthorizeHandler is the handler for the /authorize route
// required params // required params
// ?redirect_uri = redirect url // ?redirect_uri = redirect url
@ -24,8 +64,6 @@ import (
// state[recommended] = to prevent CSRF attack (for authorizer its compulsory) // state[recommended] = to prevent CSRF attack (for authorizer its compulsory)
// code_challenge = to prevent CSRF attack // code_challenge = to prevent CSRF attack
// code_challenge_method = to prevent CSRF attack [only sh256 is supported] // code_challenge_method = to prevent CSRF attack [only sh256 is supported]
// check the flow for generating and verifying codes: https://developer.okta.com/blog/2019/08/22/okta-authjs-pkce#:~:text=PKCE%20works%20by%20having%20the,is%20called%20the%20Code%20Challenge.
func AuthorizeHandler() gin.HandlerFunc { func AuthorizeHandler() gin.HandlerFunc {
return func(gc *gin.Context) { return func(gc *gin.Context) {
redirectURI := strings.TrimSpace(gc.Query("redirect_uri")) redirectURI := strings.TrimSpace(gc.Query("redirect_uri"))
@ -34,8 +72,8 @@ func AuthorizeHandler() gin.HandlerFunc {
codeChallenge := strings.TrimSpace(gc.Query("code_challenge")) codeChallenge := strings.TrimSpace(gc.Query("code_challenge"))
scopeString := strings.TrimSpace(gc.Query("scope")) scopeString := strings.TrimSpace(gc.Query("scope"))
clientID := strings.TrimSpace(gc.Query("client_id")) clientID := strings.TrimSpace(gc.Query("client_id"))
template := "authorize.tmpl"
responseMode := strings.TrimSpace(gc.Query("response_mode")) responseMode := strings.TrimSpace(gc.Query("response_mode"))
nonce := strings.TrimSpace(gc.Query("nonce"))
var scope []string var scope []string
if scopeString == "" { if scopeString == "" {
@ -45,176 +83,97 @@ func AuthorizeHandler() gin.HandlerFunc {
} }
if responseMode == "" { if responseMode == "" {
responseMode = "query" responseMode = constants.ResponseModeQuery
}
if responseMode != "query" && responseMode != "web_message" {
log.Debug("Invalid response_mode: ", responseMode)
gc.JSON(400, gin.H{"error": "invalid response mode"})
} }
if redirectURI == "" { if redirectURI == "" {
redirectURI = "/app" redirectURI = "/app"
} }
isQuery := responseMode == "query"
loginURL := "/app?state=" + state + "&scope=" + strings.Join(scope, " ") + "&redirect_uri=" + redirectURI
if clientID == "" {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
log.Debug("Failed to get client_id: ", clientID)
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "client_id is required",
},
},
})
}
return
}
if client, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyClientID); client != clientID || err != nil {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
log.Debug("Invalid client_id: ", clientID)
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "invalid_client_id",
},
},
})
}
return
}
if state == "" {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
log.Debug("Failed to get state: ", state)
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "state is required",
},
},
})
}
return
}
if responseType == "" { if responseType == "" {
responseType = "token" responseType = "token"
} }
isResponseTypeCode := responseType == "code" if err := validateAuthorizeRequest(responseType, responseMode, clientID, state, codeChallenge); err != nil {
isResponseTypeToken := responseType == "token" log.Debug("invalid authorization request: ", err)
gc.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
if !isResponseTypeCode && !isResponseTypeToken {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
log.Debug("Invalid response_type: ", responseType)
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "response_type is invalid",
},
},
})
}
return return
} }
if isResponseTypeCode { code := uuid.New().String()
if codeChallenge == "" { if nonce == "" {
if isQuery { nonce = uuid.New().String()
gc.Redirect(http.StatusFound, loginURL)
} else {
log.Debug("Failed to get code_challenge: ", codeChallenge)
gc.HTML(http.StatusBadRequest, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "code_challenge is required",
},
},
})
} }
return
log := log.WithFields(log.Fields{
"response_mode": responseMode,
"response_type": responseType,
})
// TODO add state with timeout
// used for response mode query or fragment
loginState := "state=" + state + "&scope=" + strings.Join(scope, " ") + "&redirect_uri=" + redirectURI
if responseType == constants.ResponseTypeCode {
loginState += "&code=" + code
if err := memorystore.Provider.SetState(state, code+"@@"+codeChallenge); err != nil {
log.Debug("Error setting temp code", err)
}
} else {
loginState += "&nonce=" + nonce
if err := memorystore.Provider.SetState(state, nonce); err != nil {
log.Debug("Error setting temp code", err)
} }
} }
sessionToken, err := cookie.GetSession(gc) loginURL := "/app?" + loginState
if err != nil {
if isQuery { if responseMode == constants.ResponseModeFragment {
gc.Redirect(http.StatusFound, loginURL) loginURL = "/app#" + loginState
} else { }
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI, if responseType == constants.ResponseTypeCode && codeChallenge == "" {
"authorization_response": map[string]interface{}{ handleResponse(gc, responseMode, loginURL, redirectURI, map[string]interface{}{
"type": "authorization_response", "type": "authorization_response",
"response": map[string]string{ "response": map[string]interface{}{
"error": "code_challenge_required",
"error_description": "code challenge is required",
},
}, http.StatusOK)
}
loginError := map[string]interface{}{
"type": "authorization_response",
"response": map[string]interface{}{
"error": "login_required", "error": "login_required",
"error_description": "Login is required", "error_description": "Login is required",
}, },
},
})
} }
sessionToken, err := cookie.GetSession(gc)
if err != nil {
log.Debug("GetSession failed: ", err)
handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK)
return return
} }
// get session from cookie // get session from cookie
claims, err := token.ValidateBrowserSession(gc, sessionToken) claims, err := token.ValidateBrowserSession(gc, sessionToken)
if err != nil { if err != nil {
if isQuery { log.Debug("ValidateBrowserSession failed: ", err)
gc.Redirect(http.StatusFound, loginURL) handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK)
} else {
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "login_required",
"error_description": "Login is required",
},
},
})
}
return return
} }
userID := claims.Subject userID := claims.Subject
user, err := db.Provider.GetUserByID(gc, userID) user, err := db.Provider.GetUserByID(gc, userID)
if err != nil { if err != nil {
if isQuery { log.Debug("GetUserByID failed: ", err)
gc.Redirect(http.StatusFound, loginURL) handleResponse(gc, responseMode, loginURL, redirectURI, map[string]interface{}{
} else {
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response", "type": "authorization_response",
"response": map[string]string{ "response": map[string]interface{}{
"error": "signup_required", "error": "signup_required",
"error_description": "Sign up required", "error_description": "Sign up required",
}, },
}, }, http.StatusOK)
})
}
return return
} }
@ -223,81 +182,102 @@ func AuthorizeHandler() gin.HandlerFunc {
sessionKey = claims.LoginMethod + ":" + user.ID sessionKey = claims.LoginMethod + ":" + user.ID
} }
// if user is logged in
// based on the response type code, generate the response
if isResponseTypeCode {
// rollover the session for security // rollover the session for security
go memorystore.Provider.DeleteUserSession(sessionKey, claims.Nonce) go memorystore.Provider.DeleteUserSession(sessionKey, claims.Nonce)
nonce := uuid.New().String() if responseType == constants.ResponseTypeCode {
newSessionTokenData, newSessionToken, err := token.CreateSessionToken(user, nonce, claims.Roles, scope, claims.LoginMethod) newSessionTokenData, newSessionToken, err := token.CreateSessionToken(user, nonce, claims.Roles, scope, claims.LoginMethod)
if err != nil { if err != nil {
if isQuery { log.Debug("CreateSessionToken failed: ", err)
gc.Redirect(http.StatusFound, loginURL) handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK)
} else { return
gc.HTML(http.StatusOK, template, gin.H{ }
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{ // TODO: add state with timeout
"type": "authorization_response", // if err := memorystore.Provider.SetState(codeChallenge, code+"@"+newSessionToken); err != nil {
"response": map[string]string{ // log.Debug("SetState failed: ", err)
"error": "login_required", // handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK)
"error_description": "Login is required", // return
}, // }
},
}) // TODO: add state with timeout
} if err := memorystore.Provider.SetState(code, codeChallenge+"@@"+newSessionToken); err != nil {
log.Debug("SetState failed: ", err)
handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK)
return
}
if err := memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+newSessionTokenData.Nonce, newSessionToken); err != nil {
log.Debug("SetUserSession failed: ", err)
handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK)
return return
} }
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+newSessionTokenData.Nonce, newSessionToken)
cookie.SetSession(gc, newSessionToken) cookie.SetSession(gc, newSessionToken)
code := uuid.New().String()
memorystore.Provider.SetState(codeChallenge, code+"@"+newSessionToken) // in case, response type is code and user is already logged in send the code and state
gc.HTML(http.StatusOK, template, gin.H{ // and cookie session will already be rolled over and set
"target_origin": redirectURI, // gc.HTML(http.StatusOK, authorizeWebMessageTemplate, gin.H{
"authorization_response": map[string]interface{}{ // "target_origin": redirectURI,
// "authorization_response": map[string]interface{}{
// "type": "authorization_response",
// "response": map[string]string{
// "code": code,
// "state": state,
// },
// },
// })
params := "code=" + code + "&state=" + state + "&nonce=" + nonce
if responseMode == constants.ResponseModeQuery {
if strings.Contains(redirectURI, "?") {
redirectURI = redirectURI + "&" + params
} else {
redirectURI = redirectURI + "?" + params
}
} else if responseMode == constants.ResponseModeFragment {
if strings.Contains(redirectURI, "#") {
redirectURI = redirectURI + "&" + params
} else {
redirectURI = redirectURI + "#" + params
}
}
handleResponse(gc, responseMode, loginURL, redirectURI, map[string]interface{}{
"type": "authorization_response", "type": "authorization_response",
"response": map[string]string{ "response": map[string]interface{}{
"code": code, "code": code,
"state": state, "state": state,
}, },
}, }, http.StatusOK)
})
return return
} }
if isResponseTypeToken { if responseType == constants.ResponseTypeToken || responseType == constants.ResponseTypeIDToken {
// rollover the session for security // rollover the session for security
authToken, err := token.CreateAuthToken(gc, user, claims.Roles, scope, claims.LoginMethod) authToken, err := token.CreateAuthToken(gc, user, claims.Roles, scope, claims.LoginMethod, nonce, "")
if err != nil { if err != nil {
if isQuery { log.Debug("CreateAuthToken failed: ", err)
gc.Redirect(http.StatusFound, loginURL) handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK)
} else { return
gc.HTML(http.StatusOK, template, gin.H{ }
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{ if err := memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+nonce, authToken.FingerPrintHash); err != nil {
"type": "authorization_response", log.Debug("SetUserSession failed: ", err)
"response": map[string]string{ handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK)
"error": "login_required", return
"error_description": "Login is required", }
},
}, if err := memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeAccessToken+"_"+nonce, authToken.FingerPrintHash); err != nil {
}) log.Debug("SetUserSession failed: ", err)
} handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK)
return return
} }
go memorystore.Provider.DeleteUserSession(sessionKey, claims.Nonce)
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+authToken.FingerPrint, authToken.FingerPrintHash)
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token)
cookie.SetSession(gc, authToken.FingerPrintHash) cookie.SetSession(gc, authToken.FingerPrintHash)
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
if expiresIn <= 0 {
expiresIn = 1
}
// used of query mode // used of query mode
params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(authToken.IDToken.ExpiresAt, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token
res := map[string]interface{}{ res := map[string]interface{}{
"access_token": authToken.AccessToken.Token, "access_token": authToken.AccessToken.Token,
@ -305,7 +285,12 @@ func AuthorizeHandler() gin.HandlerFunc {
"state": state, "state": state,
"scope": scope, "scope": scope,
"token_type": "Bearer", "token_type": "Bearer",
"expires_in": expiresIn, "expires_in": authToken.AccessToken.ExpiresAt,
}
if nonce != "" {
params += "&nonce=" + nonce
res["nonce"] = nonce
} }
if authToken.RefreshToken != nil { if authToken.RefreshToken != nil {
@ -314,38 +299,77 @@ func AuthorizeHandler() gin.HandlerFunc {
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token) memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token)
} }
if isQuery { if responseMode == constants.ResponseModeQuery {
if strings.Contains(redirectURI, "?") { if strings.Contains(redirectURI, "?") {
gc.Redirect(http.StatusFound, redirectURI+"&"+params) redirectURI = redirectURI + "&" + params
} else { } else {
gc.Redirect(http.StatusFound, redirectURI+"?"+params) redirectURI = redirectURI + "?" + params
} }
} else if responseMode == constants.ResponseModeFragment {
if strings.Contains(redirectURI, "#") {
redirectURI = redirectURI + "&" + params
} else { } else {
gc.HTML(http.StatusOK, template, gin.H{ redirectURI = redirectURI + "#" + params
"target_origin": redirectURI, }
"authorization_response": map[string]interface{}{ }
handleResponse(gc, responseMode, loginURL, redirectURI, map[string]interface{}{
"type": "authorization_response", "type": "authorization_response",
"response": res, "response": res,
}, }, http.StatusOK)
})
}
return return
} }
if isQuery { handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK)
gc.Redirect(http.StatusFound, loginURL) }
} else { }
// by default return with error
gc.HTML(http.StatusOK, template, gin.H{ func validateAuthorizeRequest(responseType, responseMode, clientID, state, codeChallenge string) error {
if strings.TrimSpace(state) == "" {
return fmt.Errorf("invalid state. state is required to prevent csrf attack", responseMode)
}
if responseType != constants.ResponseTypeCode && responseType != constants.ResponseTypeToken && responseType != constants.ResponseTypeIDToken {
return fmt.Errorf("invalid response type %s. 'code' & 'token' are valid response_type", responseMode)
}
if responseMode != constants.ResponseModeQuery && responseMode != constants.ResponseModeWebMessage && responseMode != constants.ResponseModeFragment && responseMode != constants.ResponseModeFormPost {
return fmt.Errorf("invalid response mode %s. 'query', 'fragment', 'form_post' and 'web_message' are valid response_mode", responseMode)
}
if client, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyClientID); client != clientID || err != nil {
return fmt.Errorf("invalid client_id %s", clientID)
}
return nil
}
func handleResponse(gc *gin.Context, responseMode, loginURI, redirectURI string, data map[string]interface{}, httpStatusCode int) {
isAuthenticationRequired := false
if _, ok := data["response"].(map[string]interface{})["error"]; ok {
isAuthenticationRequired = true
}
if isAuthenticationRequired {
gc.Redirect(http.StatusFound, loginURI)
return
}
switch responseMode {
case constants.ResponseModeQuery, constants.ResponseModeFragment:
gc.Redirect(http.StatusFound, redirectURI)
return
case constants.ResponseModeWebMessage:
gc.HTML(httpStatusCode, authorizeWebMessageTemplate, gin.H{
"target_origin": redirectURI, "target_origin": redirectURI,
"authorization_response": map[string]interface{}{ "authorization_response": data,
"type": "authorization_response",
"response": map[string]string{
"error": "login_required",
"error_description": "Login is required",
},
},
}) })
} return
case constants.ResponseModeFormPost:
gc.HTML(httpStatusCode, authorizeFormPostTemplate, gin.H{
"target_origin": redirectURI,
"authorization_response": data["response"],
})
return
} }
} }

View File

@ -13,6 +13,7 @@ import (
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -55,20 +56,20 @@ func OAuthCallbackHandler() gin.HandlerFunc {
scopes := strings.Split(sessionSplit[3], ",") scopes := strings.Split(sessionSplit[3], ",")
user := models.User{} user := models.User{}
code := ctx.Request.FormValue("code") oauthCode := ctx.Request.FormValue("code")
switch provider { switch provider {
case constants.AuthRecipeMethodGoogle: case constants.AuthRecipeMethodGoogle:
user, err = processGoogleUserInfo(code) user, err = processGoogleUserInfo(oauthCode)
case constants.AuthRecipeMethodGithub: case constants.AuthRecipeMethodGithub:
user, err = processGithubUserInfo(code) user, err = processGithubUserInfo(oauthCode)
case constants.AuthRecipeMethodFacebook: case constants.AuthRecipeMethodFacebook:
user, err = processFacebookUserInfo(code) user, err = processFacebookUserInfo(oauthCode)
case constants.AuthRecipeMethodLinkedIn: case constants.AuthRecipeMethodLinkedIn:
user, err = processLinkedInUserInfo(code) user, err = processLinkedInUserInfo(oauthCode)
case constants.AuthRecipeMethodApple: case constants.AuthRecipeMethodApple:
user, err = processAppleUserInfo(code) user, err = processAppleUserInfo(oauthCode)
case constants.AuthRecipeMethodTwitter: case constants.AuthRecipeMethodTwitter:
user, err = processTwitterUserInfo(code, sessionState) user, err = processTwitterUserInfo(oauthCode, sessionState)
default: default:
log.Info("Invalid oauth provider") log.Info("Invalid oauth provider")
err = fmt.Errorf(`invalid oauth provider`) err = fmt.Errorf(`invalid oauth provider`)
@ -196,18 +197,53 @@ func OAuthCallbackHandler() gin.HandlerFunc {
} }
} }
authToken, err := token.CreateAuthToken(ctx, user, inputRoles, scopes, provider) // TODO
// use stateValue to get code / nonce
// add code / nonce to id_token
code := ""
codeChallenge := ""
nonce := ""
if stateValue != "" {
// Get state from store
authorizeState, _ := memorystore.Provider.GetState(stateValue)
if authorizeState != "" {
authorizeStateSplit := strings.Split(authorizeState, "@@")
if len(authorizeStateSplit) > 1 {
code = authorizeStateSplit[0]
codeChallenge = authorizeStateSplit[1]
} else {
nonce = authorizeState
}
go memorystore.Provider.RemoveState(stateValue)
}
}
if nonce == "" {
nonce = uuid.New().String()
}
authToken, err := token.CreateAuthToken(ctx, user, inputRoles, scopes, provider, nonce, code)
if err != nil { if err != nil {
log.Debug("Failed to create auth token: ", err) log.Debug("Failed to create auth token: ", err)
ctx.JSON(500, gin.H{"error": err.Error()}) ctx.JSON(500, gin.H{"error": err.Error()})
} }
// Code challenge could be optional if PKCE flow is not used
if code != "" {
if err := memorystore.Provider.SetState(code, codeChallenge+"@@"+authToken.FingerPrintHash); err != nil {
log.Debug("SetState failed: ", err)
ctx.JSON(500, gin.H{"error": err.Error()})
}
}
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
if expiresIn <= 0 { if expiresIn <= 0 {
expiresIn = 1 expiresIn = 1
} }
params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + stateValue + "&id_token=" + authToken.IDToken.Token params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + stateValue + "&id_token=" + authToken.IDToken.Token + "&nonce=" + nonce
if code != "" {
params += "&code=" + code
}
sessionKey := provider + ":" + user.ID sessionKey := provider + ":" + user.ID
cookie.SetSession(ctx, authToken.FingerPrintHash) cookie.SetSession(ctx, authToken.FingerPrintHash)
@ -215,7 +251,7 @@ func OAuthCallbackHandler() gin.HandlerFunc {
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token) memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token)
if authToken.RefreshToken != nil { if authToken.RefreshToken != nil {
params = params + `&refresh_token=` + authToken.RefreshToken.Token params += `&refresh_token=` + authToken.RefreshToken.Token
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token) memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token)
} }

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
@ -17,12 +18,22 @@ import (
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
) )
type RequestBody struct {
CodeVerifier string `form:"code_verifier" json:"code_verifier"`
Code string `form:"code" json:"code"`
ClientID string `form:"client_id" json:"client_id"`
ClientSecret string `form:"client_secret" json:"client_secret"`
GrantType string `form:"grant_type" json:"grant_type"`
RefreshToken string `form:"refresh_token" json:"refresh_token"`
RedirectURI string `form:"redirect_uri" json:"redirect_uri"`
}
// TokenHandler to handle /oauth/token requests // TokenHandler to handle /oauth/token requests
// grant type required // grant type required
func TokenHandler() gin.HandlerFunc { func TokenHandler() gin.HandlerFunc {
return func(gc *gin.Context) { return func(gc *gin.Context) {
var reqBody map[string]string var reqBody RequestBody
if err := gc.BindJSON(&reqBody); err != nil { if err := gc.Bind(&reqBody); err != nil {
log.Debug("Error binding JSON: ", err) log.Debug("Error binding JSON: ", err)
gc.JSON(http.StatusBadRequest, gin.H{ gc.JSON(http.StatusBadRequest, gin.H{
"error": "error_binding_json", "error": "error_binding_json",
@ -31,11 +42,12 @@ func TokenHandler() gin.HandlerFunc {
return return
} }
codeVerifier := strings.TrimSpace(reqBody["code_verifier"]) codeVerifier := strings.TrimSpace(reqBody.CodeVerifier)
code := strings.TrimSpace(reqBody["code"]) code := strings.TrimSpace(reqBody.Code)
clientID := strings.TrimSpace(reqBody["client_id"]) clientID := strings.TrimSpace(reqBody.ClientID)
grantType := strings.TrimSpace(reqBody["grant_type"]) grantType := strings.TrimSpace(reqBody.GrantType)
refreshToken := strings.TrimSpace(reqBody["refresh_token"]) refreshToken := strings.TrimSpace(reqBody.RefreshToken)
clientSecret := strings.TrimSpace(reqBody.ClientSecret)
if grantType == "" { if grantType == "" {
grantType = "authorization_code" grantType = "authorization_code"
@ -76,15 +88,6 @@ func TokenHandler() gin.HandlerFunc {
sessionKey := "" sessionKey := ""
if isAuthorizationCodeGrant { if isAuthorizationCodeGrant {
if codeVerifier == "" {
log.Debug("Code verifier is empty")
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code_verifier",
"error_description": "The code verifier is required",
})
return
}
if code == "" { if code == "" {
log.Debug("Code is empty") log.Debug("Code is empty")
gc.JSON(http.StatusBadRequest, gin.H{ gc.JSON(http.StatusBadRequest, gin.H{
@ -94,14 +97,37 @@ func TokenHandler() gin.HandlerFunc {
return return
} }
hash := sha256.New() if codeVerifier == "" && clientSecret == "" {
hash.Write([]byte(codeVerifier)) gc.JSON(http.StatusBadRequest, gin.H{
encryptedCode := strings.ReplaceAll(base64.URLEncoding.EncodeToString(hash.Sum(nil)), "+", "-") "error": "invalid_dat",
encryptedCode = strings.ReplaceAll(encryptedCode, "/", "_") "error_description": "The code verifier or client secret is required",
encryptedCode = strings.ReplaceAll(encryptedCode, "=", "") })
sessionData, err := memorystore.Provider.GetState(encryptedCode) return
}
// Get state
sessionData, err := memorystore.Provider.GetState(code)
if sessionData == "" || err != nil { if sessionData == "" || err != nil {
log.Debug("Session data is empty") log.Debug("Session data is empty")
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code",
"error_description": "The code is invalid",
})
return
}
// [0] -> code_challenge
// [1] -> session cookie
sessionDataSplit := strings.Split(sessionData, "@@")
go memorystore.Provider.RemoveState(code)
if codeVerifier != "" {
hash := sha256.New()
hash.Write([]byte(codeVerifier))
encryptedCode := strings.ReplaceAll(base64.RawURLEncoding.EncodeToString(hash.Sum(nil)), "+", "-")
encryptedCode = strings.ReplaceAll(encryptedCode, "/", "_")
encryptedCode = strings.ReplaceAll(encryptedCode, "=", "")
if encryptedCode != sessionDataSplit[0] {
gc.JSON(http.StatusBadRequest, gin.H{ gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code_verifier", "error": "invalid_code_verifier",
"error_description": "The code verifier is invalid", "error_description": "The code verifier is invalid",
@ -109,19 +135,16 @@ func TokenHandler() gin.HandlerFunc {
return return
} }
go memorystore.Provider.RemoveState(encryptedCode) } else {
// split session data if clientHash, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyClientSecret); clientSecret != clientHash || err != nil {
// it contains code@sessiontoken log.Debug("Client Secret is invalid: ", clientID)
sessionDataSplit := strings.Split(sessionData, "@")
if sessionDataSplit[0] != code {
log.Debug("Invalid code verifier. Unable to split session data")
gc.JSON(http.StatusBadRequest, gin.H{ gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code_verifier", "error": "invalid_client_secret",
"error_description": "The code verifier is invalid", "error_description": "The client secret is invalid",
}) })
return return
} }
}
// validate session // validate session
claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1]) claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1])
@ -146,6 +169,7 @@ func TokenHandler() gin.HandlerFunc {
} }
go memorystore.Provider.DeleteUserSession(sessionKey, claims.Nonce) go memorystore.Provider.DeleteUserSession(sessionKey, claims.Nonce)
} else { } else {
// validate refresh token // validate refresh token
if refreshToken == "" { if refreshToken == "" {
@ -206,7 +230,8 @@ func TokenHandler() gin.HandlerFunc {
return return
} }
authToken, err := token.CreateAuthToken(gc, user, roles, scope, loginMethod) nonce := uuid.New().String() + "@@" + code
authToken, err := token.CreateAuthToken(gc, user, roles, scope, loginMethod, nonce, code)
if err != nil { if err != nil {
log.Debug("Error creating auth token: ", err) log.Debug("Error creating auth token: ", err)
gc.JSON(http.StatusUnauthorized, gin.H{ gc.JSON(http.StatusUnauthorized, gin.H{

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
@ -98,7 +99,30 @@ func VerifyEmailHandler() gin.HandlerFunc {
if verificationRequest.Identifier == constants.VerificationTypeMagicLinkLogin { if verificationRequest.Identifier == constants.VerificationTypeMagicLinkLogin {
loginMethod = constants.AuthRecipeMethodMagicLinkLogin loginMethod = constants.AuthRecipeMethodMagicLinkLogin
} }
authToken, err := token.CreateAuthToken(c, user, roles, scope, loginMethod)
code := ""
// Not required as /oauth/token cannot be resumed from other tab
// codeChallenge := ""
nonce := ""
if state != "" {
// Get state from store
authorizeState, _ := memorystore.Provider.GetState(state)
if authorizeState != "" {
authorizeStateSplit := strings.Split(authorizeState, "@@")
if len(authorizeStateSplit) > 1 {
code = authorizeStateSplit[0]
// Not required as /oauth/token cannot be resumed from other tab
// codeChallenge = authorizeStateSplit[1]
} else {
nonce = authorizeState
}
go memorystore.Provider.RemoveState(state)
}
}
if nonce == "" {
nonce = uuid.New().String()
}
authToken, err := token.CreateAuthToken(c, user, roles, scope, loginMethod, nonce, code)
if err != nil { if err != nil {
log.Debug("Error creating auth token: ", err) log.Debug("Error creating auth token: ", err)
errorRes["error_description"] = err.Error() errorRes["error_description"] = err.Error()
@ -106,12 +130,27 @@ func VerifyEmailHandler() gin.HandlerFunc {
return return
} }
// Code challenge could be optional if PKCE flow is not used
// Not required as /oauth/token cannot be resumed from other tab
// if code != "" {
// if err := memorystore.Provider.SetState(code, codeChallenge+"@@"+authToken.FingerPrintHash); err != nil {
// log.Debug("Error setting code state ", err)
// errorRes["error_description"] = err.Error()
// c.JSON(500, errorRes)
// return
// }
// }
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
if expiresIn <= 0 { if expiresIn <= 0 {
expiresIn = 1 expiresIn = 1
} }
params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token + "&nonce=" + nonce
if code != "" {
params += "&code=" + code
}
sessionKey := loginMethod + ":" + user.ID sessionKey := loginMethod + ":" + user.ID
cookie.SetSession(c, authToken.FingerPrintHash) cookie.SetSession(c, authToken.FingerPrintHash)

View File

@ -91,7 +91,7 @@ func GetDomainName(uri string) string {
return host return host
} }
// GetAppURL to get /app/ url if not configured by user // GetAppURL to get /app url if not configured by user
func GetAppURL(gc *gin.Context) string { func GetAppURL(gc *gin.Context) string {
envAppURL, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAppURL) envAppURL, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
if envAppURL == "" || err != nil { if envAppURL == "" || err != nil {

View File

@ -94,7 +94,7 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
return res, err return res, err
} }
// exec it as go routine so that we can reduce the api latency // execute it as go routine so that we can reduce the api latency
go email.SendEmail([]string{params.Email}, constants.VerificationTypeForgotPassword, map[string]interface{}{ go email.SendEmail([]string{params.Email}, constants.VerificationTypeForgotPassword, map[string]interface{}{
"user": user.ToMap(), "user": user.ToMap(),
"organization": utils.GetOrganization(), "organization": utils.GetOrganization(),

View File

@ -6,6 +6,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -140,12 +141,43 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
}, nil }, nil
} }
authToken, err := token.CreateAuthToken(gc, user, roles, scope, constants.AuthRecipeMethodBasicAuth) code := ""
codeChallenge := ""
nonce := ""
if params.State != nil {
// Get state from store
authorizeState, _ := memorystore.Provider.GetState(refs.StringValue(params.State))
if authorizeState != "" {
authorizeStateSplit := strings.Split(authorizeState, "@@")
if len(authorizeStateSplit) > 1 {
code = authorizeStateSplit[0]
codeChallenge = authorizeStateSplit[1]
} else {
nonce = authorizeState
}
go memorystore.Provider.RemoveState(refs.StringValue(params.State))
}
}
if nonce == "" {
nonce = uuid.New().String()
}
authToken, err := token.CreateAuthToken(gc, user, roles, scope, constants.AuthRecipeMethodBasicAuth, nonce, code)
if err != nil { if err != nil {
log.Debug("Failed to create auth token", err) log.Debug("Failed to create auth token", err)
return res, err return res, err
} }
// TODO add to other login options as well
// Code challenge could be optional if PKCE flow is not used
if code != "" {
if err := memorystore.Provider.SetState(code, codeChallenge+"@@"+authToken.FingerPrintHash); err != nil {
log.Debug("SetState failed: ", err)
return res, err
}
}
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
if expiresIn <= 0 { if expiresIn <= 0 {
expiresIn = 1 expiresIn = 1

View File

@ -15,6 +15,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators" "github.com/authorizerdev/authorizer/server/validators"
@ -185,7 +186,7 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
} }
redirectURLParams := "&roles=" + strings.Join(inputRoles, ",") redirectURLParams := "&roles=" + strings.Join(inputRoles, ",")
if params.State != nil { if params.State != nil {
redirectURLParams = redirectURLParams + "&state=" + *params.State redirectURLParams = redirectURLParams + "&state=" + refs.StringValue(params.State)
} }
if params.Scope != nil && len(params.Scope) > 0 { if params.Scope != nil && len(params.Scope) > 0 {
redirectURLParams = redirectURLParams + "&scope=" + strings.Join(params.Scope, " ") redirectURLParams = redirectURLParams + "&scope=" + strings.Join(params.Scope, " ")

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
@ -70,7 +71,8 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod
scope = params.Scope scope = params.Scope
} }
authToken, err := token.CreateAuthToken(gc, user, claimRoles, scope, claims.LoginMethod) nonce := uuid.New().String()
authToken, err := token.CreateAuthToken(gc, user, claimRoles, scope, claims.LoginMethod, nonce, "")
if err != nil { if err != nil {
log.Debug("Failed to create auth token: ", err) log.Debug("Failed to create auth token: ", err)
return res, err return res, err

View File

@ -6,6 +6,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
@ -242,12 +243,42 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
scope = params.Scope scope = params.Scope
} }
authToken, err := token.CreateAuthToken(gc, user, roles, scope, constants.AuthRecipeMethodBasicAuth) code := ""
codeChallenge := ""
nonce := ""
if params.State != nil {
// Get state from store
authorizeState, _ := memorystore.Provider.GetState(refs.StringValue(params.State))
if authorizeState != "" {
authorizeStateSplit := strings.Split(authorizeState, "@@")
if len(authorizeStateSplit) > 1 {
code = authorizeStateSplit[0]
codeChallenge = authorizeStateSplit[1]
} else {
nonce = authorizeState
}
go memorystore.Provider.RemoveState(refs.StringValue(params.State))
}
}
if nonce == "" {
nonce = uuid.New().String()
}
authToken, err := token.CreateAuthToken(gc, user, roles, scope, constants.AuthRecipeMethodBasicAuth, nonce, code)
if err != nil { if err != nil {
log.Debug("Failed to create auth token: ", err) log.Debug("Failed to create auth token: ", err)
return res, err return res, err
} }
// Code challenge could be optional if PKCE flow is not used
if code != "" {
if err := memorystore.Provider.SetState(code, codeChallenge+"@@"+authToken.FingerPrintHash); err != nil {
log.Debug("SetState failed: ", err)
return res, err
}
}
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
if expiresIn <= 0 { if expiresIn <= 0 {
expiresIn = 1 expiresIn = 1

View File

@ -125,7 +125,6 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
return res, fmt.Errorf("user with this email address already exists") return res, fmt.Errorf("user with this email address already exists")
} }
// TODO figure out how to do this
go memorystore.Provider.DeleteAllUserSessions(user.ID) go memorystore.Provider.DeleteAllUserSessions(user.ID)
hostname := parsers.GetHost(gc) hostname := parsers.GetHost(gc)

View File

@ -6,6 +6,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
@ -15,6 +16,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
@ -84,12 +86,42 @@ func VerifyEmailResolver(ctx context.Context, params model.VerifyEmailInput) (*m
roles := strings.Split(user.Roles, ",") roles := strings.Split(user.Roles, ",")
scope := []string{"openid", "email", "profile"} scope := []string{"openid", "email", "profile"}
authToken, err := token.CreateAuthToken(gc, user, roles, scope, loginMethod) code := ""
// Not required as /oauth/token cannot be resumed from other tab
// codeChallenge := ""
nonce := ""
if params.State != nil {
// Get state from store
authorizeState, _ := memorystore.Provider.GetState(refs.StringValue(params.State))
if authorizeState != "" {
authorizeStateSplit := strings.Split(authorizeState, "@@")
if len(authorizeStateSplit) > 1 {
code = authorizeStateSplit[0]
// Not required as /oauth/token cannot be resumed from other tab
// codeChallenge = authorizeStateSplit[1]
} else {
nonce = authorizeState
}
go memorystore.Provider.RemoveState(refs.StringValue(params.State))
}
}
if nonce == "" {
nonce = uuid.New().String()
}
authToken, err := token.CreateAuthToken(gc, user, roles, scope, loginMethod, nonce, code)
if err != nil { if err != nil {
log.Debug("Failed to create auth token: ", err) log.Debug("Failed to create auth token: ", err)
return res, err return res, err
} }
// Code challenge could be optional if PKCE flow is not used
// Not required as /oauth/token cannot be resumed from other tab
// if code != "" {
// if err := memorystore.Provider.SetState(code, codeChallenge+"@@"+authToken.FingerPrintHash); err != nil {
// log.Debug("SetState failed: ", err)
// return res, err
// }
// }
go func() { go func() {
if isSignUp { if isSignUp {
utils.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user) utils.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user)

View File

@ -12,8 +12,10 @@ import (
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -57,12 +59,40 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
roles := strings.Split(user.Roles, ",") roles := strings.Split(user.Roles, ",")
scope := []string{"openid", "email", "profile"} scope := []string{"openid", "email", "profile"}
authToken, err := token.CreateAuthToken(gc, user, roles, scope, loginMethod) code := ""
codeChallenge := ""
nonce := ""
if params.State != nil {
// Get state from store
authorizeState, _ := memorystore.Provider.GetState(refs.StringValue(params.State))
if authorizeState != "" {
authorizeStateSplit := strings.Split(authorizeState, "@@")
if len(authorizeStateSplit) > 1 {
code = authorizeStateSplit[0]
codeChallenge = authorizeStateSplit[1]
} else {
nonce = authorizeState
}
go memorystore.Provider.RemoveState(refs.StringValue(params.State))
}
}
if nonce == "" {
nonce = uuid.New().String()
}
authToken, err := token.CreateAuthToken(gc, user, roles, scope, loginMethod, nonce, code)
if err != nil { if err != nil {
log.Debug("Failed to create auth token: ", err) log.Debug("Failed to create auth token: ", err)
return res, err return res, err
} }
// Code challenge could be optional if PKCE flow is not used
if code != "" {
if err := memorystore.Provider.SetState(code, codeChallenge+"@@"+authToken.FingerPrintHash); err != nil {
log.Debug("Failed to set code state: ", err)
return res, err
}
}
go func() { go func() {
db.Provider.DeleteOTP(gc, otp) db.Provider.DeleteOTP(gc, otp)
if isSignUp { if isSignUp {

View File

@ -51,7 +51,8 @@ func validateJwtTokenTest(t *testing.T, s TestSetup) {
gc, err := utils.GinContextFromContext(ctx) gc, err := utils.GinContextFromContext(ctx)
assert.NoError(t, err) assert.NoError(t, err)
sessionKey := constants.AuthRecipeMethodBasicAuth + ":" + user.ID sessionKey := constants.AuthRecipeMethodBasicAuth + ":" + user.ID
authToken, err := token.CreateAuthToken(gc, user, roles, scope, constants.AuthRecipeMethodBasicAuth) nonce := uuid.New().String()
authToken, err := token.CreateAuthToken(gc, user, roles, scope, constants.AuthRecipeMethodBasicAuth, nonce, "")
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+authToken.FingerPrint, authToken.FingerPrintHash) memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+authToken.FingerPrint, authToken.FingerPrintHash)
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token) memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token)

View File

@ -1,6 +1,8 @@
package token package token
import ( import (
"crypto/sha256"
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings" "strings"
@ -10,7 +12,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
"github.com/google/uuid"
"github.com/robertkrimen/otto" "github.com/robertkrimen/otto"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
@ -47,30 +48,9 @@ type SessionData struct {
LoginMethod string `json:"login_method"` LoginMethod string `json:"login_method"`
} }
// CreateSessionToken creates a new session token
func CreateSessionToken(user models.User, nonce string, roles, scope []string, loginMethod string) (*SessionData, string, error) {
fingerPrintMap := &SessionData{
Nonce: nonce,
Roles: roles,
Subject: user.ID,
Scope: scope,
LoginMethod: loginMethod,
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().AddDate(1, 0, 0).Unix(),
}
fingerPrintBytes, _ := json.Marshal(fingerPrintMap)
fingerPrintHash, err := crypto.EncryptAES(string(fingerPrintBytes))
if err != nil {
return nil, "", err
}
return fingerPrintMap, fingerPrintHash, nil
}
// CreateAuthToken creates a new auth token when userlogs in // CreateAuthToken creates a new auth token when userlogs in
func CreateAuthToken(gc *gin.Context, user models.User, roles, scope []string, loginMethod string) (*Token, error) { func CreateAuthToken(gc *gin.Context, user models.User, roles, scope []string, loginMethod, nonce string, code string) (*Token, error) {
hostname := parsers.GetHost(gc) hostname := parsers.GetHost(gc)
nonce := uuid.New().String()
_, fingerPrintHash, err := CreateSessionToken(user, nonce, roles, scope, loginMethod) _, fingerPrintHash, err := CreateSessionToken(user, nonce, roles, scope, loginMethod)
if err != nil { if err != nil {
return nil, err return nil, err
@ -80,7 +60,23 @@ func CreateAuthToken(gc *gin.Context, user models.User, roles, scope []string, l
return nil, err return nil, err
} }
idToken, idTokenExpiresAt, err := CreateIDToken(user, roles, hostname, nonce, loginMethod) atHash := sha256.New()
atHash.Write([]byte(accessToken))
atHashBytes := atHash.Sum(nil)
// hashedToken := string(bs)
atHashDigest := atHashBytes[0 : len(atHashBytes)/2]
atHashString := base64.RawURLEncoding.EncodeToString(atHashDigest)
codeHashString := ""
if code != "" {
codeHash := sha256.New()
codeHash.Write([]byte(code))
codeHashBytes := codeHash.Sum(nil)
codeHashDigest := codeHashBytes[0 : len(codeHashBytes)/2]
codeHashString = base64.RawURLEncoding.EncodeToString(codeHashDigest)
}
idToken, idTokenExpiresAt, err := CreateIDToken(user, roles, hostname, nonce, atHashString, codeHashString, loginMethod)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -104,6 +100,26 @@ func CreateAuthToken(gc *gin.Context, user models.User, roles, scope []string, l
return res, nil return res, nil
} }
// CreateSessionToken creates a new session token
func CreateSessionToken(user models.User, nonce string, roles, scope []string, loginMethod string) (*SessionData, string, error) {
fingerPrintMap := &SessionData{
Nonce: nonce,
Roles: roles,
Subject: user.ID,
Scope: scope,
LoginMethod: loginMethod,
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().AddDate(1, 0, 0).Unix(),
}
fingerPrintBytes, _ := json.Marshal(fingerPrintMap)
fingerPrintHash, err := crypto.EncryptAES(string(fingerPrintBytes))
if err != nil {
return nil, "", err
}
return fingerPrintMap, fingerPrintHash, nil
}
// CreateRefreshToken util to create JWT token // CreateRefreshToken util to create JWT token
func CreateRefreshToken(user models.User, roles, scopes []string, hostname, nonce, loginMethod string) (string, int64, error) { func CreateRefreshToken(user models.User, roles, scopes []string, hostname, nonce, loginMethod string) (string, int64, error) {
// expires in 1 year // expires in 1 year
@ -318,7 +334,9 @@ func ValidateBrowserSession(gc *gin.Context, encryptedSession string) (*SessionD
// CreateIDToken util to create JWT token, based on // CreateIDToken util to create JWT token, based on
// user information, roles config and CUSTOM_ACCESS_TOKEN_SCRIPT // user information, roles config and CUSTOM_ACCESS_TOKEN_SCRIPT
func CreateIDToken(user models.User, roles []string, hostname, nonce, loginMethod string) (string, int64, error) { // For response_type (code) / authorization_code grant nonce should be empty
// for implicit flow it should be present to verify with actual state
func CreateIDToken(user models.User, roles []string, hostname, nonce, atHash, cHash, loginMethod string) (string, int64, error) {
expireTime, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAccessTokenExpiryTime) expireTime, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAccessTokenExpiryTime)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
@ -344,10 +362,10 @@ func CreateIDToken(user models.User, roles []string, hostname, nonce, loginMetho
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
customClaims := jwt.MapClaims{ customClaims := jwt.MapClaims{
"iss": hostname, "iss": hostname,
"aud": clientID, "aud": clientID,
"nonce": nonce,
"sub": user.ID, "sub": user.ID,
"exp": expiresAt, "exp": expiresAt,
"iat": time.Now().Unix(), "iat": time.Now().Unix(),
@ -357,6 +375,16 @@ func CreateIDToken(user models.User, roles []string, hostname, nonce, loginMetho
claimKey: roles, claimKey: roles,
} }
// split nonce to see if its authorization code grant method
if cHash != "" {
customClaims["at_hash"] = atHash
customClaims["c_hash"] = cHash
} else {
customClaims["nonce"] = nonce
customClaims["at_hash"] = atHash
}
for k, v := range userMap { for k, v := range userMap {
if k != "roles" { if k != "roles" {
customClaims[k] = v customClaims[k] = v

View File

@ -7,8 +7,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// TODO re-name GinContextKey -> GinContext
// GinContext to get gin context from context // GinContext to get gin context from context
func GinContextFromContext(ctx context.Context) (*gin.Context, error) { func GinContextFromContext(ctx context.Context) (*gin.Context, error) {
ginContext := ctx.Value("GinContextKey") ginContext := ctx.Value("GinContextKey")

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>Authorization Response</title>
</head>
<body onload="document.forms['authorize_form_post'].submit()">
<form action="{{.target_origin}}" name="authorize_form_post" method="POST">
{{ range $key, $val := .authorization_response }}
<input type="hidden" key="{{$key}}" value="{{$val}}" name="{{$key}}" id="{{$key}}" />
{{ end }}
</form>
</body>
</html>