diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3fa6802..c7919ab 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -2,16 +2,16 @@ on: workflow_dispatch: inputs: logLevel: - description: "Log level" + description: 'Log level' required: true - default: "warning" + default: 'warning' type: choice options: - info - warning - debug tags: - description: "Tags" + description: 'Tags' required: false type: boolean release: @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v2 with: - node-version: "16" + node-version: '16' - # Add support for more platforms with QEMU (optional) # https://github.com/docker/setup-qemu-action name: Set up QEMU @@ -36,7 +36,7 @@ jobs: platforms: linux/amd64,linux/arm64 - uses: actions/setup-go@v2 with: - go-version: "^1.17.3" + go-version: '^1.19.1' - name: Install dependencies run: | sudo apt-get install build-essential wget zip libc6-dev-arm64-cross && \ diff --git a/app/.prettierrc.json b/app/.prettierrc.json index c9de62a..8eb506e 100644 --- a/app/.prettierrc.json +++ b/app/.prettierrc.json @@ -2,5 +2,5 @@ "tabWidth": 2, "singleQuote": true, "trailingComma": "all", - "useTabs": false + "useTabs": true } diff --git a/app/package-lock.json b/app/package-lock.json index 657df4a..42e0140 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "^1.1.2", + "@authorizerdev/authorizer-react": "^1.1.3-beta.1", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", @@ -27,9 +27,9 @@ } }, "node_modules/@authorizerdev/authorizer-js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.1.0.tgz", - "integrity": "sha512-MdEw1SjhIm7pXq20AscHSbnAta2PC3w7GNBY52/OzmlBXUGH3ooUQX/aszbYOse3FlhapcrGrRvg4sNM7faGAg==", + "version": "1.1.2-beta.1", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.1.2-beta.1.tgz", + "integrity": "sha512-u+O2iB3tqF1HtdJ6LfBXL9iMycqlCCL3othBQkqitGP1ldhASWLJ2pcXZAcHgyoeczKdj2XKZKdIcWB3GYR0IQ==", "dependencies": { "cross-fetch": "^3.1.5" }, @@ -38,11 +38,11 @@ } }, "node_modules/@authorizerdev/authorizer-react": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.2.tgz", - "integrity": "sha512-uBmuKnOVX8gp8CEUuGJuz04ep+8qMEzJXWd5leEGKYMIgolHpu/lOinnMUXhjh8YL3pA4+EhvB+hQXxUX+rRHQ==", + "version": "1.1.3-beta.1", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.3-beta.1.tgz", + "integrity": "sha512-+ZsOBp6XjZVnDyeJCXgaqZ8xzFO7ygpHB6v2cblCKIA3wX5pg/Dsg1oumHGrSHIEK8No/GOtCjSx4Rv6/CweBQ==", "dependencies": { - "@authorizerdev/authorizer-js": "^1.1.0", + "@authorizerdev/authorizer-js": "^1.1.2-beta.1", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" @@ -876,19 +876,19 @@ }, "dependencies": { "@authorizerdev/authorizer-js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.1.0.tgz", - "integrity": "sha512-MdEw1SjhIm7pXq20AscHSbnAta2PC3w7GNBY52/OzmlBXUGH3ooUQX/aszbYOse3FlhapcrGrRvg4sNM7faGAg==", + "version": "1.1.2-beta.1", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.1.2-beta.1.tgz", + "integrity": "sha512-u+O2iB3tqF1HtdJ6LfBXL9iMycqlCCL3othBQkqitGP1ldhASWLJ2pcXZAcHgyoeczKdj2XKZKdIcWB3GYR0IQ==", "requires": { "cross-fetch": "^3.1.5" } }, "@authorizerdev/authorizer-react": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.2.tgz", - "integrity": "sha512-uBmuKnOVX8gp8CEUuGJuz04ep+8qMEzJXWd5leEGKYMIgolHpu/lOinnMUXhjh8YL3pA4+EhvB+hQXxUX+rRHQ==", + "version": "1.1.3-beta.1", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.3-beta.1.tgz", + "integrity": "sha512-+ZsOBp6XjZVnDyeJCXgaqZ8xzFO7ygpHB6v2cblCKIA3wX5pg/Dsg1oumHGrSHIEK8No/GOtCjSx4Rv6/CweBQ==", "requires": { - "@authorizerdev/authorizer-js": "^1.1.0", + "@authorizerdev/authorizer-js": "^1.1.2-beta.1", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" diff --git a/app/package.json b/app/package.json index 07f4063..491f105 100644 --- a/app/package.json +++ b/app/package.json @@ -6,13 +6,13 @@ "scripts": { "build": "rm -rf build && NODE_ENV=production 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": [], "author": "Lakhan Samani", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "^1.1.2", + "@authorizerdev/authorizer-react": "^1.1.3", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", diff --git a/app/src/Root.tsx b/app/src/Root.tsx index 88cec21..61dd2a8 100644 --- a/app/src/Root.tsx +++ b/app/src/Root.tsx @@ -38,6 +38,8 @@ export default function Root({ const scope = searchParams.get('scope') ? searchParams.get('scope')?.toString().split(' ') : ['openid', 'profile', 'email']; + const code = searchParams.get('code') || ''; + const nonce = searchParams.get('nonce') || ''; const urlProps: Record = { state, @@ -58,9 +60,19 @@ export default function Root({ if (token) { 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}`; + + if (code !== '') { + params += `&code=${code}`; + } + + if (nonce !== '') { + params += `&nonce=${nonce}`; + } + if (token.refresh_token) { params += `&refresh_token=${token.refresh_token}`; } + const url = new URL(redirectURL); if (redirectURL.includes('?')) { redirectURL = `${redirectURL}&${params}`; @@ -74,7 +86,7 @@ export default function Root({ } } return () => {}; - }, [token]); + }, [token, config]); if (loading) { return

Loading...

; @@ -100,7 +112,7 @@ export default function Root({ - + diff --git a/dashboard/.prettierrc.json b/dashboard/.prettierrc.json index c9de62a..8eb506e 100644 --- a/dashboard/.prettierrc.json +++ b/dashboard/.prettierrc.json @@ -2,5 +2,5 @@ "tabWidth": 2, "singleQuote": true, "trailingComma": "all", - "useTabs": false + "useTabs": true } diff --git a/server/constants/oauth2.go b/server/constants/oauth2.go new file mode 100644 index 0000000..f3e0a67 --- /dev/null +++ b/server/constants/oauth2.go @@ -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" +) diff --git a/server/go.mod b/server/go.mod index ca7f949..fbcf5ab 100644 --- a/server/go.mod +++ b/server/go.mod @@ -7,9 +7,8 @@ require ( github.com/arangodb/go-driver v1.2.1 github.com/aws/aws-sdk-go v1.44.109 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/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-redis/redis/v8 v8.11.0 github.com/goccy/go-json v0.9.11 // indirect @@ -19,7 +18,6 @@ require ( github.com/google/uuid v1.3.0 github.com/guregu/dynamo v1.16.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/pelletier/go-toml/v2 v2.0.5 // indirect github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f diff --git a/server/go.sum b/server/go.sum index 3a9f6d3..f1a0545 100644 --- a/server/go.sum +++ b/server/go.sum @@ -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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 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-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 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/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/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.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 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.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.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/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= 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.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.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/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 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-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.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/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-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.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/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-20190608224051-11cab39313c9/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/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/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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 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.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 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/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= 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/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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.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-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-20190510104115-cbcb75029529/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-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-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/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 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/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/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/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.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74= 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 h1:CgvzRniUdG67hBAzsxDGOAuq4Te1osVMYsa1eQbd4fs= diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 2701ee7..7ada9f2 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -2250,6 +2250,10 @@ input SignUpInput { scope: [String!] redirect_uri: String 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 { @@ -2257,15 +2261,27 @@ input LoginInput { password: String! roles: [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 { 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 { email: 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 { @@ -2413,10 +2429,18 @@ input DeleteEmailTemplateRequest { input VerifyOTPRequest { email: 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 { 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 { @@ -14455,7 +14479,7 @@ func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj in asMap[k] = v } - fieldsInOrder := [...]string{"email", "password", "roles", "scope"} + fieldsInOrder := [...]string{"email", "password", "roles", "scope", "state"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -14494,6 +14518,14 @@ func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj in if err != nil { 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 } - fieldsInOrder := [...]string{"email"} + fieldsInOrder := [...]string{"email", "state"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -14674,6 +14706,14 @@ func (ec *executionContext) unmarshalInputResendOTPRequest(ctx context.Context, if err != nil { 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 } - fieldsInOrder := [...]string{"email", "identifier"} + fieldsInOrder := [...]string{"email", "identifier", "state"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -14710,6 +14750,14 @@ func (ec *executionContext) unmarshalInputResendVerifyEmailInput(ctx context.Con if err != nil { 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 } - 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 { v, ok := asMap[k] if !ok { @@ -14930,6 +14978,14 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj i if err != nil { 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 } - fieldsInOrder := [...]string{"token"} + fieldsInOrder := [...]string{"token", "state"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -15830,6 +15886,14 @@ func (ec *executionContext) unmarshalInputVerifyEmailInput(ctx context.Context, if err != nil { 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 } - fieldsInOrder := [...]string{"email", "otp"} + fieldsInOrder := [...]string{"email", "otp", "state"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -15866,6 +15930,14 @@ func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context, if err != nil { 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 + } } } diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index e8d8d1a..ce44f87 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -151,6 +151,7 @@ type LoginInput struct { Password string `json:"password"` Roles []string `json:"roles"` Scope []string `json:"scope"` + State *string `json:"state"` } type MagicLinkLoginInput struct { @@ -199,12 +200,14 @@ type PaginationInput struct { } type ResendOTPRequest struct { - Email string `json:"email"` + Email string `json:"email"` + State *string `json:"state"` } type ResendVerifyEmailInput struct { - Email string `json:"email"` - Identifier string `json:"identifier"` + Email string `json:"email"` + Identifier string `json:"identifier"` + State *string `json:"state"` } type ResetPasswordInput struct { @@ -238,6 +241,7 @@ type SignUpInput struct { Scope []string `json:"scope"` RedirectURI *string `json:"redirect_uri"` IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"` + State *string `json:"state"` } type TestEndpointRequest struct { @@ -408,12 +412,14 @@ type VerificationRequests struct { } type VerifyEmailInput struct { - Token string `json:"token"` + Token string `json:"token"` + State *string `json:"state"` } type VerifyOTPRequest struct { - Email string `json:"email"` - Otp string `json:"otp"` + Email string `json:"email"` + Otp string `json:"otp"` + State *string `json:"state"` } type Webhook struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 0c48dce..457b6b0 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -6,499 +6,527 @@ scalar Map scalar Any type Pagination { - limit: Int64! - page: Int64! - offset: Int64! - total: Int64! + limit: Int64! + page: Int64! + offset: Int64! + total: Int64! } type Meta { - version: String! - client_id: String! - is_google_login_enabled: Boolean! - is_facebook_login_enabled: Boolean! - is_github_login_enabled: Boolean! - is_linkedin_login_enabled: Boolean! - is_apple_login_enabled: Boolean! - is_twitter_login_enabled: Boolean! - is_email_verification_enabled: Boolean! - is_basic_authentication_enabled: Boolean! - is_magic_link_login_enabled: Boolean! - is_sign_up_enabled: Boolean! - is_strong_password_enabled: Boolean! - is_multi_factor_auth_enabled: Boolean! + version: String! + client_id: String! + is_google_login_enabled: Boolean! + is_facebook_login_enabled: Boolean! + is_github_login_enabled: Boolean! + is_linkedin_login_enabled: Boolean! + is_apple_login_enabled: Boolean! + is_twitter_login_enabled: Boolean! + is_email_verification_enabled: Boolean! + is_basic_authentication_enabled: Boolean! + is_magic_link_login_enabled: Boolean! + is_sign_up_enabled: Boolean! + is_strong_password_enabled: Boolean! + is_multi_factor_auth_enabled: Boolean! } type User { - id: ID! - email: String! - email_verified: Boolean! - signup_methods: String! - given_name: String - family_name: String - middle_name: String - nickname: String - # defaults to email - preferred_username: String - gender: String - birthdate: String - phone_number: String - phone_number_verified: Boolean - picture: String - roles: [String!]! - created_at: Int64 - updated_at: Int64 - revoked_timestamp: Int64 - is_multi_factor_auth_enabled: Boolean + id: ID! + email: String! + email_verified: Boolean! + signup_methods: String! + given_name: String + family_name: String + middle_name: String + nickname: String + # defaults to email + preferred_username: String + gender: String + birthdate: String + phone_number: String + phone_number_verified: Boolean + picture: String + roles: [String!]! + created_at: Int64 + updated_at: Int64 + revoked_timestamp: Int64 + is_multi_factor_auth_enabled: Boolean } type Users { - pagination: Pagination! - users: [User!]! + pagination: Pagination! + users: [User!]! } type VerificationRequest { - id: ID! - identifier: String - token: String - email: String - expires: Int64 - created_at: Int64 - updated_at: Int64 - nonce: String - redirect_uri: String + id: ID! + identifier: String + token: String + email: String + expires: Int64 + created_at: Int64 + updated_at: Int64 + nonce: String + redirect_uri: String } type VerificationRequests { - pagination: Pagination! - verification_requests: [VerificationRequest!]! + pagination: Pagination! + verification_requests: [VerificationRequest!]! } type Error { - message: String! - reason: String! + message: String! + reason: String! } type AuthResponse { - message: String! - should_show_otp_screen: Boolean - access_token: String - id_token: String - refresh_token: String - expires_in: Int64 - user: User + message: String! + should_show_otp_screen: Boolean + access_token: String + id_token: String + refresh_token: String + expires_in: Int64 + user: User } type Response { - message: String! + message: String! } type Env { - ACCESS_TOKEN_EXPIRY_TIME: String - ADMIN_SECRET: String - DATABASE_NAME: String - DATABASE_URL: String - DATABASE_TYPE: String - DATABASE_USERNAME: String - DATABASE_PASSWORD: String - DATABASE_HOST: String - DATABASE_PORT: String - CLIENT_ID: String! - CLIENT_SECRET: String! - CUSTOM_ACCESS_TOKEN_SCRIPT: String - SMTP_HOST: String - SMTP_PORT: String - SMTP_USERNAME: String - SMTP_PASSWORD: String - SMTP_LOCAL_NAME: String - SENDER_EMAIL: String - JWT_TYPE: String - JWT_SECRET: String - JWT_PRIVATE_KEY: String - JWT_PUBLIC_KEY: String - ALLOWED_ORIGINS: [String!] - APP_URL: String - REDIS_URL: String - RESET_PASSWORD_URL: String - DISABLE_EMAIL_VERIFICATION: Boolean! - DISABLE_BASIC_AUTHENTICATION: Boolean! - DISABLE_MAGIC_LINK_LOGIN: Boolean! - DISABLE_LOGIN_PAGE: Boolean! - DISABLE_SIGN_UP: Boolean! - DISABLE_REDIS_FOR_ENV: Boolean! - DISABLE_STRONG_PASSWORD: Boolean! - DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean! - ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean! - ROLES: [String!] - PROTECTED_ROLES: [String!] - DEFAULT_ROLES: [String!] - JWT_ROLE_CLAIM: String - GOOGLE_CLIENT_ID: String - GOOGLE_CLIENT_SECRET: String - GITHUB_CLIENT_ID: String - GITHUB_CLIENT_SECRET: String - FACEBOOK_CLIENT_ID: String - FACEBOOK_CLIENT_SECRET: String - LINKEDIN_CLIENT_ID: String - LINKEDIN_CLIENT_SECRET: String - APPLE_CLIENT_ID: String - APPLE_CLIENT_SECRET: String - TWITTER_CLIENT_ID: String - TWITTER_CLIENT_SECRET: String - ORGANIZATION_NAME: String - ORGANIZATION_LOGO: String - APP_COOKIE_SECURE: Boolean! - ADMIN_COOKIE_SECURE: Boolean! + ACCESS_TOKEN_EXPIRY_TIME: String + ADMIN_SECRET: String + DATABASE_NAME: String + DATABASE_URL: String + DATABASE_TYPE: String + DATABASE_USERNAME: String + DATABASE_PASSWORD: String + DATABASE_HOST: String + DATABASE_PORT: String + CLIENT_ID: String! + CLIENT_SECRET: String! + CUSTOM_ACCESS_TOKEN_SCRIPT: String + SMTP_HOST: String + SMTP_PORT: String + SMTP_USERNAME: String + SMTP_PASSWORD: String + SMTP_LOCAL_NAME: String + SENDER_EMAIL: String + JWT_TYPE: String + JWT_SECRET: String + JWT_PRIVATE_KEY: String + JWT_PUBLIC_KEY: String + ALLOWED_ORIGINS: [String!] + APP_URL: String + REDIS_URL: String + RESET_PASSWORD_URL: String + DISABLE_EMAIL_VERIFICATION: Boolean! + DISABLE_BASIC_AUTHENTICATION: Boolean! + DISABLE_MAGIC_LINK_LOGIN: Boolean! + DISABLE_LOGIN_PAGE: Boolean! + DISABLE_SIGN_UP: Boolean! + DISABLE_REDIS_FOR_ENV: Boolean! + DISABLE_STRONG_PASSWORD: Boolean! + DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean! + ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean! + ROLES: [String!] + PROTECTED_ROLES: [String!] + DEFAULT_ROLES: [String!] + JWT_ROLE_CLAIM: String + GOOGLE_CLIENT_ID: String + GOOGLE_CLIENT_SECRET: String + GITHUB_CLIENT_ID: String + GITHUB_CLIENT_SECRET: String + FACEBOOK_CLIENT_ID: String + FACEBOOK_CLIENT_SECRET: String + LINKEDIN_CLIENT_ID: String + LINKEDIN_CLIENT_SECRET: String + APPLE_CLIENT_ID: String + APPLE_CLIENT_SECRET: String + TWITTER_CLIENT_ID: String + TWITTER_CLIENT_SECRET: String + ORGANIZATION_NAME: String + ORGANIZATION_LOGO: String + APP_COOKIE_SECURE: Boolean! + ADMIN_COOKIE_SECURE: Boolean! } type ValidateJWTTokenResponse { - is_valid: Boolean! - claims: Map + is_valid: Boolean! + claims: Map } type GenerateJWTKeysResponse { - secret: String - public_key: String - private_key: String + secret: String + public_key: String + private_key: String } type Webhook { - id: ID! - event_name: String - endpoint: String - enabled: Boolean - headers: Map - created_at: Int64 - updated_at: Int64 + id: ID! + event_name: String + endpoint: String + enabled: Boolean + headers: Map + created_at: Int64 + updated_at: Int64 } type Webhooks { - pagination: Pagination! - webhooks: [Webhook!]! + pagination: Pagination! + webhooks: [Webhook!]! } type WebhookLog { - id: ID! - http_status: Int64 - response: String - request: String - webhook_id: ID - created_at: Int64 - updated_at: Int64 + id: ID! + http_status: Int64 + response: String + request: String + webhook_id: ID + created_at: Int64 + updated_at: Int64 } type TestEndpointResponse { - http_status: Int64 - response: String + http_status: Int64 + response: String } type WebhookLogs { - pagination: Pagination! - webhook_logs: [WebhookLog!]! + pagination: Pagination! + webhook_logs: [WebhookLog!]! } type EmailTemplate { - id: ID! - event_name: String! - template: String! - design: String! - subject: String! - created_at: Int64 - updated_at: Int64 + id: ID! + event_name: String! + template: String! + design: String! + subject: String! + created_at: Int64 + updated_at: Int64 } type EmailTemplates { - pagination: Pagination! - email_templates: [EmailTemplate!]! + pagination: Pagination! + email_templates: [EmailTemplate!]! } input UpdateEnvInput { - ACCESS_TOKEN_EXPIRY_TIME: String - ADMIN_SECRET: String - CUSTOM_ACCESS_TOKEN_SCRIPT: String - OLD_ADMIN_SECRET: String - SMTP_HOST: String - SMTP_PORT: String - SMTP_USERNAME: String - SMTP_PASSWORD: String - SMTP_LOCAL_NAME: String - SENDER_EMAIL: String - JWT_TYPE: String - JWT_SECRET: String - JWT_PRIVATE_KEY: String - JWT_PUBLIC_KEY: String - ALLOWED_ORIGINS: [String!] - APP_URL: String - RESET_PASSWORD_URL: String - APP_COOKIE_SECURE: Boolean - ADMIN_COOKIE_SECURE: Boolean - DISABLE_EMAIL_VERIFICATION: Boolean - DISABLE_BASIC_AUTHENTICATION: Boolean - DISABLE_MAGIC_LINK_LOGIN: Boolean - DISABLE_LOGIN_PAGE: Boolean - DISABLE_SIGN_UP: Boolean - DISABLE_REDIS_FOR_ENV: Boolean - DISABLE_STRONG_PASSWORD: Boolean - DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean - ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean - ROLES: [String!] - PROTECTED_ROLES: [String!] - DEFAULT_ROLES: [String!] - JWT_ROLE_CLAIM: String - GOOGLE_CLIENT_ID: String - GOOGLE_CLIENT_SECRET: String - GITHUB_CLIENT_ID: String - GITHUB_CLIENT_SECRET: String - FACEBOOK_CLIENT_ID: String - FACEBOOK_CLIENT_SECRET: String - LINKEDIN_CLIENT_ID: String - LINKEDIN_CLIENT_SECRET: String - APPLE_CLIENT_ID: String - APPLE_CLIENT_SECRET: String - TWITTER_CLIENT_ID: String - TWITTER_CLIENT_SECRET: String - ORGANIZATION_NAME: String - ORGANIZATION_LOGO: String + ACCESS_TOKEN_EXPIRY_TIME: String + ADMIN_SECRET: String + CUSTOM_ACCESS_TOKEN_SCRIPT: String + OLD_ADMIN_SECRET: String + SMTP_HOST: String + SMTP_PORT: String + SMTP_USERNAME: String + SMTP_PASSWORD: String + SMTP_LOCAL_NAME: String + SENDER_EMAIL: String + JWT_TYPE: String + JWT_SECRET: String + JWT_PRIVATE_KEY: String + JWT_PUBLIC_KEY: String + ALLOWED_ORIGINS: [String!] + APP_URL: String + RESET_PASSWORD_URL: String + APP_COOKIE_SECURE: Boolean + ADMIN_COOKIE_SECURE: Boolean + DISABLE_EMAIL_VERIFICATION: Boolean + DISABLE_BASIC_AUTHENTICATION: Boolean + DISABLE_MAGIC_LINK_LOGIN: Boolean + DISABLE_LOGIN_PAGE: Boolean + DISABLE_SIGN_UP: Boolean + DISABLE_REDIS_FOR_ENV: Boolean + DISABLE_STRONG_PASSWORD: Boolean + DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean + ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean + ROLES: [String!] + PROTECTED_ROLES: [String!] + DEFAULT_ROLES: [String!] + JWT_ROLE_CLAIM: String + GOOGLE_CLIENT_ID: String + GOOGLE_CLIENT_SECRET: String + GITHUB_CLIENT_ID: String + GITHUB_CLIENT_SECRET: String + FACEBOOK_CLIENT_ID: String + FACEBOOK_CLIENT_SECRET: String + LINKEDIN_CLIENT_ID: String + LINKEDIN_CLIENT_SECRET: String + APPLE_CLIENT_ID: String + APPLE_CLIENT_SECRET: String + TWITTER_CLIENT_ID: String + TWITTER_CLIENT_SECRET: String + ORGANIZATION_NAME: String + ORGANIZATION_LOGO: String } input AdminLoginInput { - admin_secret: String! + admin_secret: String! } input AdminSignupInput { - admin_secret: String! + admin_secret: String! } input SignUpInput { - email: String! - given_name: String - family_name: String - middle_name: String - nickname: String - gender: String - birthdate: String - phone_number: String - picture: String - password: String! - confirm_password: String! - roles: [String!] - scope: [String!] - redirect_uri: String - is_multi_factor_auth_enabled: Boolean + email: String! + given_name: String + family_name: String + middle_name: String + nickname: String + gender: String + birthdate: String + phone_number: String + picture: String + password: String! + confirm_password: String! + roles: [String!] + scope: [String!] + redirect_uri: String + 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 { - email: String! - password: String! - roles: [String!] - scope: [String!] + email: String! + password: String! + roles: [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 { - 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 { - email: String! - identifier: String! + email: 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 { - old_password: String - new_password: String - confirm_new_password: String - email: String - given_name: String - family_name: String - middle_name: String - nickname: String - gender: String - birthdate: String - phone_number: String - picture: String - is_multi_factor_auth_enabled: Boolean + old_password: String + new_password: String + confirm_new_password: String + email: String + given_name: String + family_name: String + middle_name: String + nickname: String + gender: String + birthdate: String + phone_number: String + picture: String + is_multi_factor_auth_enabled: Boolean } input UpdateUserInput { - id: ID! - email: String - email_verified: Boolean - given_name: String - family_name: String - middle_name: String - nickname: String - gender: String - birthdate: String - phone_number: String - picture: String - roles: [String] - is_multi_factor_auth_enabled: Boolean + id: ID! + email: String + email_verified: Boolean + given_name: String + family_name: String + middle_name: String + nickname: String + gender: String + birthdate: String + phone_number: String + picture: String + roles: [String] + is_multi_factor_auth_enabled: Boolean } input ForgotPasswordInput { - email: String! - state: String - redirect_uri: String + email: String! + state: String + redirect_uri: String } input ResetPasswordInput { - token: String! - password: String! - confirm_password: String! + token: String! + password: String! + confirm_password: String! } input DeleteUserInput { - email: String! + email: String! } input MagicLinkLoginInput { - email: String! - roles: [String!] - scope: [String!] - state: String - redirect_uri: String + email: String! + roles: [String!] + scope: [String!] + state: String + redirect_uri: String } input SessionQueryInput { - roles: [String!] - scope: [String!] + roles: [String!] + scope: [String!] } input PaginationInput { - limit: Int64 - page: Int64 + limit: Int64 + page: Int64 } input PaginatedInput { - pagination: PaginationInput + pagination: PaginationInput } input OAuthRevokeInput { - refresh_token: String! + refresh_token: String! } input InviteMemberInput { - emails: [String!]! - redirect_uri: String + emails: [String!]! + redirect_uri: String } input UpdateAccessInput { - user_id: String! + user_id: String! } input ValidateJWTTokenInput { - token_type: String! - token: String! - roles: [String!] + token_type: String! + token: String! + roles: [String!] } input GenerateJWTKeysInput { - type: String! + type: String! } input ListWebhookLogRequest { - pagination: PaginationInput - webhook_id: String + pagination: PaginationInput + webhook_id: String } input AddWebhookRequest { - event_name: String! - endpoint: String! - enabled: Boolean! - headers: Map + event_name: String! + endpoint: String! + enabled: Boolean! + headers: Map } input UpdateWebhookRequest { - id: ID! - event_name: String - endpoint: String - enabled: Boolean - headers: Map + id: ID! + event_name: String + endpoint: String + enabled: Boolean + headers: Map } input WebhookRequest { - id: ID! + id: ID! } input TestEndpointRequest { - endpoint: String! - event_name: String! - headers: Map + endpoint: String! + event_name: String! + headers: Map } input AddEmailTemplateRequest { - event_name: String! - subject: String! - template: String! - design: String + event_name: String! + subject: String! + template: String! + # Design value is set when editor is used + # If raw HTML is used design value is set to null + design: String } input UpdateEmailTemplateRequest { - id: ID! - event_name: String - template: String - subject: String - design: String + id: ID! + event_name: String + template: String + subject: String + # Design value is set when editor is used + # If raw HTML is used design value is set to null + design: String } input DeleteEmailTemplateRequest { - id: ID! + id: ID! } input VerifyOTPRequest { - email: String! - otp: String! + email: 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 { - 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 { - signup(params: SignUpInput!): AuthResponse! - login(params: LoginInput!): AuthResponse! - magic_link_login(params: MagicLinkLoginInput!): Response! - logout: Response! - update_profile(params: UpdateProfileInput!): Response! - verify_email(params: VerifyEmailInput!): AuthResponse! - resend_verify_email(params: ResendVerifyEmailInput!): Response! - forgot_password(params: ForgotPasswordInput!): Response! - reset_password(params: ResetPasswordInput!): Response! - revoke(params: OAuthRevokeInput!): Response! - verify_otp(params: VerifyOTPRequest!): AuthResponse! - resend_otp(params: ResendOTPRequest!): Response! - # admin only apis - _delete_user(params: DeleteUserInput!): Response! - _update_user(params: UpdateUserInput!): User! - _admin_signup(params: AdminSignupInput!): Response! - _admin_login(params: AdminLoginInput!): Response! - _admin_logout: Response! - _update_env(params: UpdateEnvInput!): Response! - _invite_members(params: InviteMemberInput!): Response! - _revoke_access(param: UpdateAccessInput!): Response! - _enable_access(param: UpdateAccessInput!): Response! - _generate_jwt_keys(params: GenerateJWTKeysInput!): GenerateJWTKeysResponse! - _add_webhook(params: AddWebhookRequest!): Response! - _update_webhook(params: UpdateWebhookRequest!): Response! - _delete_webhook(params: WebhookRequest!): Response! - _test_endpoint(params: TestEndpointRequest!): TestEndpointResponse! - _add_email_template(params: AddEmailTemplateRequest!): Response! - _update_email_template(params: UpdateEmailTemplateRequest!): Response! - _delete_email_template(params: DeleteEmailTemplateRequest!): Response! + signup(params: SignUpInput!): AuthResponse! + login(params: LoginInput!): AuthResponse! + magic_link_login(params: MagicLinkLoginInput!): Response! + logout: Response! + update_profile(params: UpdateProfileInput!): Response! + verify_email(params: VerifyEmailInput!): AuthResponse! + resend_verify_email(params: ResendVerifyEmailInput!): Response! + forgot_password(params: ForgotPasswordInput!): Response! + reset_password(params: ResetPasswordInput!): Response! + revoke(params: OAuthRevokeInput!): Response! + verify_otp(params: VerifyOTPRequest!): AuthResponse! + resend_otp(params: ResendOTPRequest!): Response! + # admin only apis + _delete_user(params: DeleteUserInput!): Response! + _update_user(params: UpdateUserInput!): User! + _admin_signup(params: AdminSignupInput!): Response! + _admin_login(params: AdminLoginInput!): Response! + _admin_logout: Response! + _update_env(params: UpdateEnvInput!): Response! + _invite_members(params: InviteMemberInput!): Response! + _revoke_access(param: UpdateAccessInput!): Response! + _enable_access(param: UpdateAccessInput!): Response! + _generate_jwt_keys(params: GenerateJWTKeysInput!): GenerateJWTKeysResponse! + _add_webhook(params: AddWebhookRequest!): Response! + _update_webhook(params: UpdateWebhookRequest!): Response! + _delete_webhook(params: WebhookRequest!): Response! + _test_endpoint(params: TestEndpointRequest!): TestEndpointResponse! + _add_email_template(params: AddEmailTemplateRequest!): Response! + _update_email_template(params: UpdateEmailTemplateRequest!): Response! + _delete_email_template(params: DeleteEmailTemplateRequest!): Response! } type Query { - meta: Meta! - session(params: SessionQueryInput): AuthResponse! - profile: User! - validate_jwt_token(params: ValidateJWTTokenInput!): ValidateJWTTokenResponse! - # admin only apis - _users(params: PaginatedInput): Users! - _verification_requests(params: PaginatedInput): VerificationRequests! - _admin_session: Response! - _env: Env! - _webhook(params: WebhookRequest!): Webhook! - _webhooks(params: PaginatedInput): Webhooks! - _webhook_logs(params: ListWebhookLogRequest): WebhookLogs! - _email_templates(params: PaginatedInput): EmailTemplates! -} \ No newline at end of file + meta: Meta! + session(params: SessionQueryInput): AuthResponse! + profile: User! + validate_jwt_token(params: ValidateJWTTokenInput!): ValidateJWTTokenResponse! + # admin only apis + _users(params: PaginatedInput): Users! + _verification_requests(params: PaginatedInput): VerificationRequests! + _admin_session: Response! + _env: Env! + _webhook(params: WebhookRequest!): Webhook! + _webhooks(params: PaginatedInput): Webhooks! + _webhook_logs(params: ListWebhookLogRequest): WebhookLogs! + _email_templates(params: PaginatedInput): EmailTemplates! +} diff --git a/server/handlers/authorize.go b/server/handlers/authorize.go index fd2372c..c3e2e2a 100644 --- a/server/handlers/authorize.go +++ b/server/handlers/authorize.go @@ -1,10 +1,41 @@ 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 ( + "fmt" "net/http" "strconv" "strings" - "time" "github.com/gin-gonic/gin" "github.com/google/uuid" @@ -17,6 +48,15 @@ import ( "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 // required params // ?redirect_uri = redirect url @@ -24,8 +64,6 @@ import ( // state[recommended] = to prevent CSRF attack (for authorizer its compulsory) // code_challenge = to prevent CSRF attack // 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 { return func(gc *gin.Context) { redirectURI := strings.TrimSpace(gc.Query("redirect_uri")) @@ -34,8 +72,8 @@ func AuthorizeHandler() gin.HandlerFunc { codeChallenge := strings.TrimSpace(gc.Query("code_challenge")) scopeString := strings.TrimSpace(gc.Query("scope")) clientID := strings.TrimSpace(gc.Query("client_id")) - template := "authorize.tmpl" responseMode := strings.TrimSpace(gc.Query("response_mode")) + nonce := strings.TrimSpace(gc.Query("nonce")) var scope []string if scopeString == "" { @@ -45,176 +83,97 @@ func AuthorizeHandler() gin.HandlerFunc { } if responseMode == "" { - responseMode = "query" - } - - if responseMode != "query" && responseMode != "web_message" { - log.Debug("Invalid response_mode: ", responseMode) - gc.JSON(400, gin.H{"error": "invalid response mode"}) + responseMode = constants.ResponseModeQuery } if redirectURI == "" { 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 == "" { responseType = "token" } - isResponseTypeCode := responseType == "code" - isResponseTypeToken := responseType == "token" - - 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", - }, - }, - }) - } + if err := validateAuthorizeRequest(responseType, responseMode, clientID, state, codeChallenge); err != nil { + log.Debug("invalid authorization request: ", err) + gc.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if isResponseTypeCode { - if codeChallenge == "" { - if isQuery { - 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 + code := uuid.New().String() + if nonce == "" { + nonce = uuid.New().String() + } + + 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) } } + loginURL := "/app?" + loginState + + if responseMode == constants.ResponseModeFragment { + loginURL = "/app#" + loginState + } + + if responseType == constants.ResponseTypeCode && codeChallenge == "" { + handleResponse(gc, responseMode, loginURL, redirectURI, map[string]interface{}{ + "type": "authorization_response", + "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_description": "Login is required", + }, + } sessionToken, err := cookie.GetSession(gc) if err != nil { - if isQuery { - gc.Redirect(http.StatusFound, loginURL) - } 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", - }, - }, - }) - } + log.Debug("GetSession failed: ", err) + handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) return } // get session from cookie claims, err := token.ValidateBrowserSession(gc, sessionToken) if err != nil { - if isQuery { - gc.Redirect(http.StatusFound, loginURL) - } 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", - }, - }, - }) - } + log.Debug("ValidateBrowserSession failed: ", err) + handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) return } + userID := claims.Subject user, err := db.Provider.GetUserByID(gc, userID) if err != nil { - if isQuery { - gc.Redirect(http.StatusFound, loginURL) - } else { - gc.HTML(http.StatusOK, template, gin.H{ - "target_origin": redirectURI, - "authorization_response": map[string]interface{}{ - "type": "authorization_response", - "response": map[string]string{ - "error": "signup_required", - "error_description": "Sign up required", - }, - }, - }) - } + log.Debug("GetUserByID failed: ", err) + handleResponse(gc, responseMode, loginURL, redirectURI, map[string]interface{}{ + "type": "authorization_response", + "response": map[string]interface{}{ + "error": "signup_required", + "error_description": "Sign up required", + }, + }, http.StatusOK) return } @@ -223,81 +182,102 @@ func AuthorizeHandler() gin.HandlerFunc { 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 - go memorystore.Provider.DeleteUserSession(sessionKey, claims.Nonce) - nonce := uuid.New().String() + // rollover the session for security + go memorystore.Provider.DeleteUserSession(sessionKey, claims.Nonce) + if responseType == constants.ResponseTypeCode { newSessionTokenData, newSessionToken, err := token.CreateSessionToken(user, nonce, claims.Roles, scope, claims.LoginMethod) if err != nil { - if isQuery { - gc.Redirect(http.StatusFound, loginURL) - } 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", - }, - }, - }) - } + log.Debug("CreateSessionToken failed: ", err) + handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) + return + } + + // TODO: add state with timeout + // if err := memorystore.Provider.SetState(codeChallenge, code+"@"+newSessionToken); err != nil { + // log.Debug("SetState failed: ", err) + // handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) + // 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 } - memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+newSessionTokenData.Nonce, newSessionToken) cookie.SetSession(gc, newSessionToken) - code := uuid.New().String() - memorystore.Provider.SetState(codeChallenge, code+"@"+newSessionToken) - gc.HTML(http.StatusOK, template, gin.H{ - "target_origin": redirectURI, - "authorization_response": map[string]interface{}{ - "type": "authorization_response", - "response": map[string]string{ - "code": code, - "state": state, - }, + + // in case, response type is code and user is already logged in send the code and state + // and cookie session will already be rolled over and set + // gc.HTML(http.StatusOK, authorizeWebMessageTemplate, gin.H{ + // "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", + "response": map[string]interface{}{ + "code": code, + "state": state, }, - }) + }, http.StatusOK) + return } - if isResponseTypeToken { + if responseType == constants.ResponseTypeToken || responseType == constants.ResponseTypeIDToken { // 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 isQuery { - gc.Redirect(http.StatusFound, loginURL) - } 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", - }, - }, - }) - } + log.Debug("CreateAuthToken failed: ", err) + handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) 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) - - expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() - if expiresIn <= 0 { - expiresIn = 1 + if err := memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+nonce, authToken.FingerPrintHash); err != nil { + log.Debug("SetUserSession failed: ", err) + handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) + return } + 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 + } + + cookie.SetSession(gc, authToken.FingerPrintHash) + // 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{}{ "access_token": authToken.AccessToken.Token, @@ -305,7 +285,12 @@ func AuthorizeHandler() gin.HandlerFunc { "state": state, "scope": scope, "token_type": "Bearer", - "expires_in": expiresIn, + "expires_in": authToken.AccessToken.ExpiresAt, + } + + if nonce != "" { + params += "&nonce=" + nonce + res["nonce"] = nonce } if authToken.RefreshToken != nil { @@ -314,38 +299,77 @@ func AuthorizeHandler() gin.HandlerFunc { memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token) } - if isQuery { + if responseMode == constants.ResponseModeQuery { if strings.Contains(redirectURI, "?") { - gc.Redirect(http.StatusFound, redirectURI+"&"+params) + redirectURI = redirectURI + "&" + params } else { - gc.Redirect(http.StatusFound, redirectURI+"?"+params) + redirectURI = redirectURI + "?" + params + } + } else if responseMode == constants.ResponseModeFragment { + if strings.Contains(redirectURI, "#") { + redirectURI = redirectURI + "&" + params + } else { + redirectURI = redirectURI + "#" + params } - } else { - gc.HTML(http.StatusOK, template, gin.H{ - "target_origin": redirectURI, - "authorization_response": map[string]interface{}{ - "type": "authorization_response", - "response": res, - }, - }) } + + handleResponse(gc, responseMode, loginURL, redirectURI, map[string]interface{}{ + "type": "authorization_response", + "response": res, + }, http.StatusOK) return } - if isQuery { - gc.Redirect(http.StatusFound, loginURL) - } else { - // by default return with error - 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", - }, - }, - }) - } + handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) + } +} + +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, + "authorization_response": data, + }) + return + case constants.ResponseModeFormPost: + gc.HTML(httpStatusCode, authorizeFormPostTemplate, gin.H{ + "target_origin": redirectURI, + "authorization_response": data["response"], + }) + return } } diff --git a/server/handlers/oauth_callback.go b/server/handlers/oauth_callback.go index 02c284c..8bd894c 100644 --- a/server/handlers/oauth_callback.go +++ b/server/handlers/oauth_callback.go @@ -13,6 +13,7 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/gin-gonic/gin" + "github.com/google/uuid" log "github.com/sirupsen/logrus" "golang.org/x/oauth2" @@ -55,20 +56,20 @@ func OAuthCallbackHandler() gin.HandlerFunc { scopes := strings.Split(sessionSplit[3], ",") user := models.User{} - code := ctx.Request.FormValue("code") + oauthCode := ctx.Request.FormValue("code") switch provider { case constants.AuthRecipeMethodGoogle: - user, err = processGoogleUserInfo(code) + user, err = processGoogleUserInfo(oauthCode) case constants.AuthRecipeMethodGithub: - user, err = processGithubUserInfo(code) + user, err = processGithubUserInfo(oauthCode) case constants.AuthRecipeMethodFacebook: - user, err = processFacebookUserInfo(code) + user, err = processFacebookUserInfo(oauthCode) case constants.AuthRecipeMethodLinkedIn: - user, err = processLinkedInUserInfo(code) + user, err = processLinkedInUserInfo(oauthCode) case constants.AuthRecipeMethodApple: - user, err = processAppleUserInfo(code) + user, err = processAppleUserInfo(oauthCode) case constants.AuthRecipeMethodTwitter: - user, err = processTwitterUserInfo(code, sessionState) + user, err = processTwitterUserInfo(oauthCode, sessionState) default: log.Info("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 { log.Debug("Failed to create auth token: ", err) 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() if expiresIn <= 0 { 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 cookie.SetSession(ctx, authToken.FingerPrintHash) @@ -215,7 +251,7 @@ func OAuthCallbackHandler() gin.HandlerFunc { memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token) 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) } diff --git a/server/handlers/token.go b/server/handlers/token.go index 4d0064d..b515dd5 100644 --- a/server/handlers/token.go +++ b/server/handlers/token.go @@ -8,6 +8,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/authorizerdev/authorizer/server/constants" @@ -17,12 +18,22 @@ import ( "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 // grant type required func TokenHandler() gin.HandlerFunc { return func(gc *gin.Context) { - var reqBody map[string]string - if err := gc.BindJSON(&reqBody); err != nil { + var reqBody RequestBody + if err := gc.Bind(&reqBody); err != nil { log.Debug("Error binding JSON: ", err) gc.JSON(http.StatusBadRequest, gin.H{ "error": "error_binding_json", @@ -31,11 +42,12 @@ func TokenHandler() gin.HandlerFunc { return } - codeVerifier := strings.TrimSpace(reqBody["code_verifier"]) - code := strings.TrimSpace(reqBody["code"]) - clientID := strings.TrimSpace(reqBody["client_id"]) - grantType := strings.TrimSpace(reqBody["grant_type"]) - refreshToken := strings.TrimSpace(reqBody["refresh_token"]) + codeVerifier := strings.TrimSpace(reqBody.CodeVerifier) + code := strings.TrimSpace(reqBody.Code) + clientID := strings.TrimSpace(reqBody.ClientID) + grantType := strings.TrimSpace(reqBody.GrantType) + refreshToken := strings.TrimSpace(reqBody.RefreshToken) + clientSecret := strings.TrimSpace(reqBody.ClientSecret) if grantType == "" { grantType = "authorization_code" @@ -76,15 +88,6 @@ func TokenHandler() gin.HandlerFunc { sessionKey := "" 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 == "" { log.Debug("Code is empty") gc.JSON(http.StatusBadRequest, gin.H{ @@ -94,33 +97,53 @@ func TokenHandler() gin.HandlerFunc { return } - hash := sha256.New() - hash.Write([]byte(codeVerifier)) - encryptedCode := strings.ReplaceAll(base64.URLEncoding.EncodeToString(hash.Sum(nil)), "+", "-") - encryptedCode = strings.ReplaceAll(encryptedCode, "/", "_") - encryptedCode = strings.ReplaceAll(encryptedCode, "=", "") - sessionData, err := memorystore.Provider.GetState(encryptedCode) + if codeVerifier == "" && clientSecret == "" { + gc.JSON(http.StatusBadRequest, gin.H{ + "error": "invalid_dat", + "error_description": "The code verifier or client secret is required", + }) + return + } + // Get state + sessionData, err := memorystore.Provider.GetState(code) if sessionData == "" || err != nil { log.Debug("Session data is empty") gc.JSON(http.StatusBadRequest, gin.H{ - "error": "invalid_code_verifier", - "error_description": "The code verifier is invalid", + "error": "invalid_code", + "error_description": "The code is invalid", }) return } - go memorystore.Provider.RemoveState(encryptedCode) - // split session data - // it contains code@sessiontoken - sessionDataSplit := strings.Split(sessionData, "@") + // [0] -> code_challenge + // [1] -> session cookie + sessionDataSplit := strings.Split(sessionData, "@@") - if sessionDataSplit[0] != code { - log.Debug("Invalid code verifier. Unable to split session data") - gc.JSON(http.StatusBadRequest, gin.H{ - "error": "invalid_code_verifier", - "error_description": "The code verifier is invalid", - }) - return + 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{ + "error": "invalid_code_verifier", + "error_description": "The code verifier is invalid", + }) + return + } + + } else { + if clientHash, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyClientSecret); clientSecret != clientHash || err != nil { + log.Debug("Client Secret is invalid: ", clientID) + gc.JSON(http.StatusBadRequest, gin.H{ + "error": "invalid_client_secret", + "error_description": "The client secret is invalid", + }) + return + } } // validate session @@ -146,6 +169,7 @@ func TokenHandler() gin.HandlerFunc { } go memorystore.Provider.DeleteUserSession(sessionKey, claims.Nonce) + } else { // validate refresh token if refreshToken == "" { @@ -206,7 +230,8 @@ func TokenHandler() gin.HandlerFunc { 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 { log.Debug("Error creating auth token: ", err) gc.JSON(http.StatusUnauthorized, gin.H{ diff --git a/server/handlers/verify_email.go b/server/handlers/verify_email.go index 10d4fad..cf5ec1a 100644 --- a/server/handlers/verify_email.go +++ b/server/handlers/verify_email.go @@ -7,6 +7,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/authorizerdev/authorizer/server/constants" @@ -98,7 +99,30 @@ func VerifyEmailHandler() gin.HandlerFunc { if verificationRequest.Identifier == constants.VerificationTypeMagicLinkLogin { 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 { log.Debug("Error creating auth token: ", err) errorRes["error_description"] = err.Error() @@ -106,12 +130,27 @@ func VerifyEmailHandler() gin.HandlerFunc { 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() if expiresIn <= 0 { 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 cookie.SetSession(c, authToken.FingerPrintHash) diff --git a/server/parsers/url.go b/server/parsers/url.go index c98ad54..48c2c79 100644 --- a/server/parsers/url.go +++ b/server/parsers/url.go @@ -91,7 +91,7 @@ func GetDomainName(uri string) string { 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 { envAppURL, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAppURL) if envAppURL == "" || err != nil { diff --git a/server/resolvers/forgot_password.go b/server/resolvers/forgot_password.go index 25a05f4..f497b31 100644 --- a/server/resolvers/forgot_password.go +++ b/server/resolvers/forgot_password.go @@ -94,7 +94,7 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu 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{}{ "user": user.ToMap(), "organization": utils.GetOrganization(), diff --git a/server/resolvers/login.go b/server/resolvers/login.go index b597ada..4bae30a 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/google/uuid" log "github.com/sirupsen/logrus" "golang.org/x/crypto/bcrypt" @@ -140,12 +141,43 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes }, 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 { log.Debug("Failed to create auth token", 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() if expiresIn <= 0 { expiresIn = 1 diff --git a/server/resolvers/magic_link_login.go b/server/resolvers/magic_link_login.go index 611c47f..bcc7ce2 100644 --- a/server/resolvers/magic_link_login.go +++ b/server/resolvers/magic_link_login.go @@ -15,6 +15,7 @@ import ( "github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/parsers" + "github.com/authorizerdev/authorizer/server/refs" "github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/validators" @@ -185,7 +186,7 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu } redirectURLParams := "&roles=" + strings.Join(inputRoles, ",") if params.State != nil { - redirectURLParams = redirectURLParams + "&state=" + *params.State + redirectURLParams = redirectURLParams + "&state=" + refs.StringValue(params.State) } if params.Scope != nil && len(params.Scope) > 0 { redirectURLParams = redirectURLParams + "&scope=" + strings.Join(params.Scope, " ") diff --git a/server/resolvers/session.go b/server/resolvers/session.go index dfa4e3d..79ea012 100644 --- a/server/resolvers/session.go +++ b/server/resolvers/session.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/authorizerdev/authorizer/server/constants" @@ -70,7 +71,8 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod 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 { log.Debug("Failed to create auth token: ", err) return res, err diff --git a/server/resolvers/signup.go b/server/resolvers/signup.go index d5cd071..43f2a96 100644 --- a/server/resolvers/signup.go +++ b/server/resolvers/signup.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/authorizerdev/authorizer/server/constants" @@ -242,12 +243,42 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR 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 { log.Debug("Failed to create auth token: ", 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() if expiresIn <= 0 { expiresIn = 1 diff --git a/server/resolvers/update_user.go b/server/resolvers/update_user.go index e2f6618..057b797 100644 --- a/server/resolvers/update_user.go +++ b/server/resolvers/update_user.go @@ -125,7 +125,6 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod return res, fmt.Errorf("user with this email address already exists") } - // TODO figure out how to do this go memorystore.Provider.DeleteAllUserSessions(user.ID) hostname := parsers.GetHost(gc) diff --git a/server/resolvers/verify_email.go b/server/resolvers/verify_email.go index 624d08a..47b4429 100644 --- a/server/resolvers/verify_email.go +++ b/server/resolvers/verify_email.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/authorizerdev/authorizer/server/constants" @@ -15,6 +16,7 @@ import ( "github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/parsers" + "github.com/authorizerdev/authorizer/server/refs" "github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/utils" ) @@ -84,12 +86,42 @@ func VerifyEmailResolver(ctx context.Context, params model.VerifyEmailInput) (*m roles := strings.Split(user.Roles, ",") 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 { log.Debug("Failed to create auth token: ", 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() { if isSignUp { utils.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user) diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go index b792adb..016678d 100644 --- a/server/resolvers/verify_otp.go +++ b/server/resolvers/verify_otp.go @@ -12,8 +12,10 @@ import ( "github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/memorystore" + "github.com/authorizerdev/authorizer/server/refs" "github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/utils" + "github.com/google/uuid" log "github.com/sirupsen/logrus" ) @@ -57,12 +59,40 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod roles := strings.Split(user.Roles, ",") 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 { log.Debug("Failed to create auth token: ", 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() { db.Provider.DeleteOTP(gc, otp) if isSignUp { diff --git a/server/test/validate_jwt_token_test.go b/server/test/validate_jwt_token_test.go index e906222..52ce50b 100644 --- a/server/test/validate_jwt_token_test.go +++ b/server/test/validate_jwt_token_test.go @@ -51,7 +51,8 @@ func validateJwtTokenTest(t *testing.T, s TestSetup) { gc, err := utils.GinContextFromContext(ctx) assert.NoError(t, err) 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.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token) diff --git a/server/token/auth_token.go b/server/token/auth_token.go index 87d164f..32d1085 100644 --- a/server/token/auth_token.go +++ b/server/token/auth_token.go @@ -1,6 +1,8 @@ package token import ( + "crypto/sha256" + "encoding/base64" "encoding/json" "fmt" "strings" @@ -10,7 +12,6 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" - "github.com/google/uuid" "github.com/robertkrimen/otto" "github.com/authorizerdev/authorizer/server/constants" @@ -47,30 +48,9 @@ type SessionData struct { 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 -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) - nonce := uuid.New().String() _, fingerPrintHash, err := CreateSessionToken(user, nonce, roles, scope, loginMethod) if err != nil { return nil, err @@ -80,7 +60,23 @@ func CreateAuthToken(gc *gin.Context, user models.User, roles, scope []string, l 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 { return nil, err } @@ -104,6 +100,26 @@ func CreateAuthToken(gc *gin.Context, user models.User, roles, scope []string, l 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 func CreateRefreshToken(user models.User, roles, scopes []string, hostname, nonce, loginMethod string) (string, int64, error) { // expires in 1 year @@ -318,7 +334,9 @@ func ValidateBrowserSession(gc *gin.Context, encryptedSession string) (*SessionD // CreateIDToken util to create JWT token, based on // 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) if err != nil { return "", 0, err @@ -344,10 +362,10 @@ func CreateIDToken(user models.User, roles []string, hostname, nonce, loginMetho if err != nil { return "", 0, err } + customClaims := jwt.MapClaims{ "iss": hostname, "aud": clientID, - "nonce": nonce, "sub": user.ID, "exp": expiresAt, "iat": time.Now().Unix(), @@ -357,6 +375,16 @@ func CreateIDToken(user models.User, roles []string, hostname, nonce, loginMetho 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 { if k != "roles" { customClaims[k] = v diff --git a/server/utils/gin_context.go b/server/utils/gin_context.go index 7e3ced6..0491cbe 100644 --- a/server/utils/gin_context.go +++ b/server/utils/gin_context.go @@ -7,8 +7,6 @@ import ( "github.com/gin-gonic/gin" ) -// TODO re-name GinContextKey -> GinContext - // GinContext to get gin context from context func GinContextFromContext(ctx context.Context) (*gin.Context, error) { ginContext := ctx.Value("GinContextKey") diff --git a/templates/authorize_form_post.tmpl b/templates/authorize_form_post.tmpl new file mode 100644 index 0000000..3098636 --- /dev/null +++ b/templates/authorize_form_post.tmpl @@ -0,0 +1,13 @@ + + + + Authorization Response + + +
+ {{ range $key, $val := .authorization_response }} + + {{ end }} +
+ + diff --git a/templates/authorize.tmpl b/templates/authorize_web_message.tmpl similarity index 100% rename from templates/authorize.tmpl rename to templates/authorize_web_message.tmpl