Compare commits

...

51 Commits

Author SHA1 Message Date
Lakhan Samani
cfbce17ab8 fix: set same site cookie to none for cross site 2022-09-28 18:42:42 +05:30
Lakhan Samani
aa6601e62c fix: same site cookie 2022-09-28 18:30:30 +05:30
Lakhan Samani
d8ea0c656f Merge pull request #247 from authorizerdev/fix/same-site-cookie
fix(server): use sameSite as lax by default for app cookie
2022-09-28 11:18:03 +05:30
Lakhan Samani
f5323e0eec fix(server): update comments for host & cookies 2022-09-28 10:36:56 +05:30
Lakhan Samani
b1bc7b5370 fix(server): set default app cookie to lax mode 2022-09-28 09:51:04 +05:30
Lakhan Samani
536fd87c3c fix: debug log 2022-09-27 06:45:38 +05:30
Lakhan Samani
f8c96a9fee Merge pull request #244 from authorizerdev/fix/remove-user-verification-request-when-deleted
fix: remove entries from otp + verification when user is deleted
2022-09-27 06:44:22 +05:30
Lakhan Samani
837fc781de fix: remove entries from otp + verification when user is deleted
Resolves #234
2022-09-27 00:27:36 +05:30
Lakhan Samani
640bb8c9ed chore: bump app/authorizer-react 1.1.2 2022-09-27 00:07:52 +05:30
Lakhan Samani
d9bba0bbe7 Merge pull request #243 from authorizerdev/fix/bool-env-vars-secure-cookie
fix: app & admin cookie secure variable type while persisting info
2022-09-27 00:03:31 +05:30
Lakhan Samani
f91ec1880f fix: app & admin cookie secure variable type while persisting info
Resolves #241
2022-09-27 00:01:38 +05:30
Lakhan Samani
19e2153379 Update README.md 2022-09-15 12:24:47 +05:30
Lakhan Samani
221009bf0a Merge pull request #229 from ruessej/main
feat: Add a option to disable httpOnly cookies
2022-09-15 11:22:27 +05:30
ruessej
6085c2d535 Fix incorrect type 2022-09-14 12:24:19 +02:00
Jerebtw
8e0c5e4380 Make the default value true 2022-09-14 11:56:48 +02:00
Lakhan Samani
21b70e4b26 Merge pull request #230 from authorizerdev/fix/github-oauth-scopes
fix: scope for github auth
2022-09-14 11:46:46 +05:30
Lakhan Samani
993693884d fix: scope for github auth 2022-09-14 11:45:38 +05:30
Lakhan Samani
ed849fa6f6 Merge branch 'main' of https://github.com/authorizerdev/authorizer 2022-09-14 10:44:09 +05:30
Lakhan Samani
aec1f5df53 fix: github endpoint to get user emails 2022-09-14 10:44:01 +05:30
ruessej
195bd1bc6a Add a option to disable httpOnly cookies 2022-09-12 14:37:42 +02:00
Lakhan Samani
45b4c41bca Merge pull request #228 from Deep-Codes/main 2022-09-10 11:40:11 +05:30
Deepankar
63d486821e fix: lint 2022-09-10 11:39:01 +05:30
Deep-Codes
4b56afdc98 fix(type): __authorizer__ on window 2022-09-10 11:23:20 +05:30
Lakhan Samani
6455ff956a fix: remove varible log 2022-09-10 10:52:56 +05:30
Lakhan Samani
3898e43fff feat: add button to jwt config as json 2022-09-10 10:50:15 +05:30
Lakhan Samani
2c305e5bde Update README.md 2022-09-09 10:24:30 +05:30
Lakhan Samani
b8fd08e576 Update README.md 2022-09-09 09:29:27 +05:30
Lakhan Samani
6dafa45051 fix: invalid login message
Resolves #224
2022-09-03 21:48:33 +05:30
Lakhan Samani
ead3514113 chore: update railway template 2022-08-31 13:09:00 +05:30
Lakhan Samani
75a413e5f2 Merge branch 'main' of https://github.com/authorizerdev/authorizer 2022-08-31 11:02:50 +05:30
Lakhan Samani
91bf0e2478 fix: use replace all 2022-08-31 11:02:46 +05:30
Lakhan Samani
7a1305cf96 Merge pull request #222 from Deep-Codes/main 2022-08-31 07:04:20 +05:30
Deep-Codes
ff5a6ec301 feat(server): add log to show PORT 2022-08-30 23:35:43 +05:30
Lakhan Samani
b7b97b4f8d Merge pull request #221 from Deep-Codes/main
fix(dashboard): users table overflow
2022-08-30 22:38:49 +05:30
Deep-Codes
d9bc989c74 fix(dashboard): users table overflow 2022-08-30 21:56:28 +05:30
Lakhan Samani
d1f80d4088 feat: add support for twitter login 2022-08-29 08:37:53 +05:30
Lakhan Samani
4b299f0da2 fix: log 2022-08-29 08:19:11 +05:30
Lakhan Samani
ed8006db4c Merge branch 'main' of https://github.com/authorizerdev/authorizer 2022-08-29 08:18:42 +05:30
Lakhan Samani
97f6c7d50a fix: authorize endpoint setting user session 2022-08-29 08:18:20 +05:30
Lakhan Samani
5e3f68a180 Merge pull request #216 from szczepad/feat/twitter-login
Feat/twitter login
2022-08-24 08:53:52 +05:30
szczepad
f73d1fc588 feat: Adds login via twitter 2022-08-22 09:25:10 +02:00
szczepad
aa232de426 fix: Uses whitespace as seperator for oauth scopes in state-string
This is necessary, as the previous delimiter (,) was being redacted
after a redirect. This resulted in the scopes not being correctly
parseable and the state not being fetched correctly after the
oauth-callback
2022-08-22 09:25:10 +02:00
Lakhan Samani
34ce754ef6 feat: bootstrap twitter login config 2022-08-22 09:03:29 +02:00
Lakhan Samani
5f385b2016 fix: remove unused file 2022-08-18 07:21:50 +05:30
Lakhan Samani
da7c17271e Merge pull request #215 from wabscale/main
fix: rootless container
2022-08-18 06:11:32 +05:30
John McCann Cunniff Jr
69fbd631ff fix: rootless container 2022-08-17 20:33:05 -04:00
Lakhan Samani
deb209e358 Update README.md 2022-08-15 22:28:43 +05:30
Lakhan Samani
ea6b4cbc8d Update README.md 2022-08-15 22:28:13 +05:30
Lakhan Samani
2f21a09b2e chore: bump app/authorizer-react 1.0.0 2022-08-15 21:06:57 +05:30
Lakhan Samani
4ab775f2c1 fix: apple & linkedin env config 2022-08-13 12:37:04 +05:30
Lakhan Samani
b6e8023104 Merge pull request #211 from authorizerdev/fix/email-template
fix email template
2022-08-13 11:58:07 +05:30
43 changed files with 1092 additions and 490 deletions

View File

@@ -21,13 +21,15 @@ RUN apk add build-base &&\
make build-dashboard make build-dashboard
FROM alpine:latest FROM alpine:latest
WORKDIR /root/ RUN adduser -D -h /authorizer -u 1000 -k /dev/null authorizer
WORKDIR /authorizer
RUN mkdir app dashboard RUN mkdir app dashboard
COPY --from=node-builder /authorizer/app/build app/build COPY --from=node-builder --chown=nobody:nobody /authorizer/app/build app/build
COPY --from=node-builder /authorizer/app/favicon_io app/favicon_io COPY --from=node-builder --chown=nobody:nobody /authorizer/app/favicon_io app/favicon_io
COPY --from=node-builder /authorizer/dashboard/build dashboard/build COPY --from=node-builder --chown=nobody:nobody /authorizer/dashboard/build dashboard/build
COPY --from=node-builder /authorizer/dashboard/favicon_io dashboard/favicon_io COPY --from=node-builder --chown=nobody:nobody /authorizer/dashboard/favicon_io dashboard/favicon_io
COPY --from=go-builder /authorizer/build build COPY --from=go-builder --chown=nobody:nobody /authorizer/build build
COPY templates templates COPY templates templates
EXPOSE 8080 EXPOSE 8080
USER authorizer
CMD [ "./build/server" ] CMD [ "./build/server" ]

View File

@@ -7,19 +7,17 @@
Authorizer Authorizer
</h1> </h1>
**Authorizer** is an open-source authentication and authorization solution for your applications. Bring your database and have complete control over the user information. You can self-host authorizer instances and connect to any database (Currently supports [Postgres](https://www.postgresql.org/), [MySQL](https://www.mysql.com/), [SQLite](https://www.sqlite.org/index.html), [SQLServer](https://www.microsoft.com/en-us/sql-server/), [MongoDB](https://mongodb.com/), [ArangoDB](https://www.arangodb.com/)). **Authorizer** is an open-source authentication and authorization solution for your applications. Bring your database and have complete control over the user information. You can self-host authorizer instances and connect to any database (Currently supports 11+ databases including [Postgres](https://www.postgresql.org/), [MySQL](https://www.mysql.com/), [SQLite](https://www.sqlite.org/index.html), [SQLServer](https://www.microsoft.com/en-us/sql-server/), [YugaByte](https://www.yugabyte.com/), [MariaDB](https://mariadb.org/), [PlanetScale](https://planetscale.com/), [CassandraDB](https://cassandra.apache.org/_/index.html), [ScyllaDB](https://www.scylladb.com/), [MongoDB](https://mongodb.com/), [ArangoDB](https://www.arangodb.com/)).
## Table of contents For more information check:
- [Introduction](#introduction)
- [Getting Started](#getting-started)
- [Contributing](https://github.com/authorizerdev/authorizer/blob/main/.github/CONTRIBUTING.md)
- [Docs](http://docs.authorizer.dev/) - [Docs](http://docs.authorizer.dev/)
- [Join Community](https://discord.gg/Zv2D5h6kkK) - [Discord Community](https://discord.gg/Zv2D5h6kkK)
- [Contributing Guide](https://github.com/authorizerdev/authorizer/blob/main/.github/CONTRIBUTING.md)
# Introduction # Introduction
<img src="https://github.com/authorizerdev/authorizer/blob/main/assets/authorizer-architecture.png" style="height:20em"/> <img src="https://docs.authorizer.dev/images/authorizer-arch.png" style="height:20em"/>
#### We offer the following functionality #### We offer the following functionality
@@ -29,20 +27,22 @@
- ✅ OAuth2 and OpenID compatible APIs - ✅ OAuth2 and OpenID compatible APIs
- ✅ APIs to update profile securely - ✅ APIs to update profile securely
- ✅ Forgot password flow using email - ✅ Forgot password flow using email
- ✅ Social logins (Google, Github, Facebook, more coming soon) - ✅ Social logins (Google, Github, Facebook, LinkedIn, Apple more coming soon)
- ✅ Role-based access management - ✅ Role-based access management
- ✅ Password-less login with magic link login - ✅ Password-less login with magic link login
- ✅ Multi factor authentication
- ✅ Email templating
- ✅ Webhooks
## Roadmap ## Roadmap
- 2 Factor authentication - [VueJS SDK](https://github.com/authorizerdev/authorizer-vue)
- VueJS SDK - [Svelte SDK](https://github.com/authorizerdev/authorizer-svelte)
- Svelte SDK - [Golang SDK](https://github.com/authorizerdev/authorizer-go)
- React Native SDK - React Native SDK
- Flutter SDK - Flutter SDK
- Android Native SDK - Android Native SDK
- iOS native SDK - iOS native SDK
- Golang SDK
- Python SDK - Python SDK
- PHP SDK - PHP SDK
- WordPress plugin - WordPress plugin
@@ -63,11 +63,11 @@
Deploy production ready Authorizer instance using one click deployment options available below Deploy production ready Authorizer instance using one click deployment options available below
| **Infra provider** | **One-click link** | **Additional information** | | **Infra provider** | **One-click link** | **Additional information** |
| :----------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------: | | :----------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------: |
| Railway.app | <a href="https://railway.app/new/template?template=https://github.com/authorizerdev/authorizer-railway&amp;plugins=postgresql,redis"><img src="https://railway.app/button.svg" style="height: 44px" alt="Deploy on Railway"></a> | [docs](https://docs.authorizer.dev/deployment/railway) | | Railway.app | <a href="https://railway.app/new/template/nwXp1C?referralCode=FEF4uT"><img src="https://railway.app/button.svg" style="height: 44px" alt="Deploy on Railway"></a> | [docs](https://docs.authorizer.dev/deployment/railway) |
| Heroku | <a href="https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku"><img src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy to Heroku" style="height: 44px;"></a> | [docs](https://docs.authorizer.dev/deployment/heroku) | | Heroku | <a href="https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku"><img src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy to Heroku" style="height: 44px;"></a> | [docs](https://docs.authorizer.dev/deployment/heroku) |
| Render | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/authorizerdev/authorizer-render) | [docs](https://docs.authorizer.dev/deployment/render) | | Render | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/authorizerdev/authorizer-render) | [docs](https://docs.authorizer.dev/deployment/render) |
### Deploy Authorizer Using Source Code ### Deploy Authorizer Using Source Code

62
app/package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^0.26.0-beta.0", "@authorizerdev/authorizer-react": "^1.1.2",
"@types/react": "^17.0.15", "@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17", "esbuild": "^0.12.17",
@@ -26,22 +26,22 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-js": { "node_modules/@authorizerdev/authorizer-js": {
"version": "0.17.0-beta.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.17.0-beta.1.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.1.0.tgz",
"integrity": "sha512-jUlFUrs4Ys6LZ5hclPeRt84teygi+bA57d/IpV9GAqOrfifv70jkFeDln4+Bs0mZk74el23Xn+DR9380mqE4Cg==", "integrity": "sha512-MdEw1SjhIm7pXq20AscHSbnAta2PC3w7GNBY52/OzmlBXUGH3ooUQX/aszbYOse3FlhapcrGrRvg4sNM7faGAg==",
"dependencies": { "dependencies": {
"node-fetch": "^2.6.1" "cross-fetch": "^3.1.5"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@authorizerdev/authorizer-react": { "node_modules/@authorizerdev/authorizer-react": {
"version": "0.26.0-beta.0", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.26.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.2.tgz",
"integrity": "sha512-YfyiGYBmbsp3tLWIxOrOZ/hUTCmdMXVE9SLE8m1xsFsxzJJlUhepp0AMahSbH5EyLj5bchOhOw/rzgpnDZDvMw==", "integrity": "sha512-uBmuKnOVX8gp8CEUuGJuz04ep+8qMEzJXWd5leEGKYMIgolHpu/lOinnMUXhjh8YL3pA4+EhvB+hQXxUX+rRHQ==",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": "^0.17.0-beta.1", "@authorizerdev/authorizer-js": "^1.1.0",
"final-form": "^4.20.2", "final-form": "^4.20.2",
"react-final-form": "^6.5.3", "react-final-form": "^6.5.3",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"
@@ -404,6 +404,14 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
}, },
"node_modules/cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"dependencies": {
"node-fetch": "2.6.7"
}
},
"node_modules/css-color-keywords": { "node_modules/css-color-keywords": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
@@ -461,9 +469,9 @@
} }
}, },
"node_modules/final-form": { "node_modules/final-form": {
"version": "4.20.6", "version": "4.20.4",
"resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.6.tgz", "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.4.tgz",
"integrity": "sha512-fCdwIj49KOaFfDRlXB57Eo+GghIMZQWrA9TakQI3C9uQxHwaFHXqZSNRlUdfnQmNNeySwGOaGPZCvjy58hyv4w==", "integrity": "sha512-hyoOVVilPLpkTvgi+FSJkFZrh0Yhy4BhE6lk/NiBwrF4aRV8/ykKEyXYvQH/pfUbRkOosvpESYouFb+FscsLrw==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.10.0" "@babel/runtime": "^7.10.0"
}, },
@@ -852,19 +860,19 @@
}, },
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": { "@authorizerdev/authorizer-js": {
"version": "0.17.0-beta.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.17.0-beta.1.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.1.0.tgz",
"integrity": "sha512-jUlFUrs4Ys6LZ5hclPeRt84teygi+bA57d/IpV9GAqOrfifv70jkFeDln4+Bs0mZk74el23Xn+DR9380mqE4Cg==", "integrity": "sha512-MdEw1SjhIm7pXq20AscHSbnAta2PC3w7GNBY52/OzmlBXUGH3ooUQX/aszbYOse3FlhapcrGrRvg4sNM7faGAg==",
"requires": { "requires": {
"node-fetch": "^2.6.1" "cross-fetch": "^3.1.5"
} }
}, },
"@authorizerdev/authorizer-react": { "@authorizerdev/authorizer-react": {
"version": "0.26.0-beta.0", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.26.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.2.tgz",
"integrity": "sha512-YfyiGYBmbsp3tLWIxOrOZ/hUTCmdMXVE9SLE8m1xsFsxzJJlUhepp0AMahSbH5EyLj5bchOhOw/rzgpnDZDvMw==", "integrity": "sha512-uBmuKnOVX8gp8CEUuGJuz04ep+8qMEzJXWd5leEGKYMIgolHpu/lOinnMUXhjh8YL3pA4+EhvB+hQXxUX+rRHQ==",
"requires": { "requires": {
"@authorizerdev/authorizer-js": "^0.17.0-beta.1", "@authorizerdev/authorizer-js": "^1.1.0",
"final-form": "^4.20.2", "final-form": "^4.20.2",
"react-final-form": "^6.5.3", "react-final-form": "^6.5.3",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"
@@ -1161,6 +1169,14 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
}, },
"cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"requires": {
"node-fetch": "2.6.7"
}
},
"css-color-keywords": { "css-color-keywords": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
@@ -1200,9 +1216,9 @@
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
}, },
"final-form": { "final-form": {
"version": "4.20.6", "version": "4.20.4",
"resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.6.tgz", "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.4.tgz",
"integrity": "sha512-fCdwIj49KOaFfDRlXB57Eo+GghIMZQWrA9TakQI3C9uQxHwaFHXqZSNRlUdfnQmNNeySwGOaGPZCvjy58hyv4w==", "integrity": "sha512-hyoOVVilPLpkTvgi+FSJkFZrh0Yhy4BhE6lk/NiBwrF4aRV8/ykKEyXYvQH/pfUbRkOosvpESYouFb+FscsLrw==",
"requires": { "requires": {
"@babel/runtime": "^7.10.0" "@babel/runtime": "^7.10.0"
} }

View File

@@ -11,7 +11,7 @@
"author": "Lakhan Samani", "author": "Lakhan Samani",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^0.26.0-beta.0", "@authorizerdev/authorizer-react": "^1.1.2",
"@types/react": "^17.0.15", "@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17", "esbuild": "^0.12.17",

View File

@@ -4,6 +4,12 @@ import { AuthorizerProvider } from '@authorizerdev/authorizer-react';
import Root from './Root'; import Root from './Root';
import { createRandomString } from './utils/common'; import { createRandomString } from './utils/common';
declare global {
interface Window {
__authorizer__: any;
}
}
export default function App() { export default function App() {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const state = searchParams.get('state') || createRandomString(); const state = searchParams.get('state') || createRandomString();
@@ -24,7 +30,6 @@ export default function App() {
urlProps.redirectURL = window.location.origin + '/app'; urlProps.redirectURL = window.location.origin + '/app';
} }
const globalState: Record<string, string> = { const globalState: Record<string, string> = {
// @ts-ignore
...window['__authorizer__'], ...window['__authorizer__'],
...urlProps, ...urlProps,
}; };

View File

@@ -1,154 +1,201 @@
import React from "react"; import React from 'react';
import { Flex, Stack, Center, Text, useMediaQuery } from "@chakra-ui/react";
import { import {
HiddenInputType, Flex,
TextInputType, Stack,
TextAreaInputType, Center,
} from "../../constants"; Text,
import GenerateKeysModal from "../GenerateKeysModal"; useMediaQuery,
import InputField from "../InputField"; Button,
useToast,
} from '@chakra-ui/react';
import {
HiddenInputType,
TextInputType,
TextAreaInputType,
} from '../../constants';
import GenerateKeysModal from '../GenerateKeysModal';
import InputField from '../InputField';
import { copyTextToClipboard } from '../../utils';
const JSTConfigurations = ({ const JSTConfigurations = ({
variables, variables,
setVariables, setVariables,
fieldVisibility, fieldVisibility,
setFieldVisibility, setFieldVisibility,
SelectInputType, SelectInputType,
getData, getData,
HMACEncryptionType, HMACEncryptionType,
RSAEncryptionType, RSAEncryptionType,
ECDSAEncryptionType, ECDSAEncryptionType,
}: any) => { }: any) => {
const [isNotSmallerScreen] = useMediaQuery("(min-width:600px)"); const [isNotSmallerScreen] = useMediaQuery('(min-width:600px)');
const toast = useToast();
return ( const copyJSON = async () => {
<div> try {
{" "} await copyTextToClipboard(
<Flex JSON.stringify({
borderRadius={5} type: variables.JWT_TYPE,
width="100%" key: variables.JWT_PUBLIC_KEY || variables.JWT_SECRET,
justifyContent="space-between" })
alignItems="center" );
paddingTop="2%" toast({
> title: `JWT config copied successfully`,
<Text isClosable: true,
fontSize={isNotSmallerScreen ? "md" : "sm"} status: 'success',
fontWeight="bold" position: 'bottom-right',
mb={5} });
> } catch (err) {
JWT (JSON Web Tokens) Configurations console.error({
</Text> message: `Failed to copy JWT config`,
<Flex mb={7}> error: err,
<GenerateKeysModal jwtType={variables.JWT_TYPE} getData={getData} /> });
</Flex> toast({
</Flex> title: `Failed to copy JWT config`,
<Stack spacing={6} padding="2% 0%"> isClosable: true,
<Flex direction={isNotSmallerScreen ? "row" : "column"}> status: 'error',
<Flex w="30%" justifyContent="start" alignItems="center"> position: 'bottom-right',
<Text fontSize="sm">JWT Type:</Text> });
</Flex> }
<Flex };
w={isNotSmallerScreen ? "70%" : "100%"}
mt={isNotSmallerScreen ? "0" : "2"} return (
> <div>
<InputField {' '}
borderRadius={5} <Flex
variables={variables} borderRadius={5}
setVariables={setVariables} width="100%"
inputType={SelectInputType} justifyContent="space-between"
value={SelectInputType} alignItems="center"
options={{ paddingTop="2%"
...HMACEncryptionType, >
...RSAEncryptionType, <Text
...ECDSAEncryptionType, fontSize={isNotSmallerScreen ? 'md' : 'sm'}
}} fontWeight="bold"
/> mb={5}
</Flex> >
</Flex> JWT (JSON Web Tokens) Configurations
{Object.values(HMACEncryptionType).includes(variables.JWT_TYPE) ? ( </Text>
<Flex direction={isNotSmallerScreen ? "row" : "column"}> <Flex mb={7}>
<Flex w="30%" justifyContent="start" alignItems="center"> <Button
<Text fontSize="sm">JWT Secret</Text> colorScheme="blue"
</Flex> h="1.75rem"
<Center size="sm"
w={isNotSmallerScreen ? "70%" : "100%"} variant="ghost"
mt={isNotSmallerScreen ? "0" : "2"} onClick={copyJSON}
> >
<InputField Copy As JSON Config
borderRadius={5} </Button>
variables={variables} <GenerateKeysModal jwtType={variables.JWT_TYPE} getData={getData} />
setVariables={setVariables} </Flex>
fieldVisibility={fieldVisibility} </Flex>
setFieldVisibility={setFieldVisibility} <Stack spacing={6} padding="2% 0%">
inputType={HiddenInputType.JWT_SECRET} <Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
/> <Flex w="30%" justifyContent="start" alignItems="center">
</Center> <Text fontSize="sm">JWT Type:</Text>
</Flex> </Flex>
) : ( <Flex
<> w={isNotSmallerScreen ? '70%' : '100%'}
<Flex direction={isNotSmallerScreen ? "row" : "column"}> mt={isNotSmallerScreen ? '0' : '2'}
<Flex w="30%" justifyContent="start" alignItems="center"> >
<Text fontSize="sm">Public Key</Text> <InputField
</Flex> borderRadius={5}
<Center variables={variables}
w={isNotSmallerScreen ? "70%" : "100%"} setVariables={setVariables}
mt={isNotSmallerScreen ? "0" : "2"} inputType={SelectInputType}
> value={SelectInputType}
<InputField options={{
borderRadius={5} ...HMACEncryptionType,
variables={variables} ...RSAEncryptionType,
setVariables={setVariables} ...ECDSAEncryptionType,
inputType={TextAreaInputType.JWT_PUBLIC_KEY} }}
placeholder="Add public key here" />
minH="25vh" </Flex>
/> </Flex>
</Center> {Object.values(HMACEncryptionType).includes(variables.JWT_TYPE) ? (
</Flex> <Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
<Flex direction={isNotSmallerScreen ? "row" : "column"}> <Flex w="30%" justifyContent="start" alignItems="center">
<Flex w="30%" justifyContent="start" alignItems="center"> <Text fontSize="sm">JWT Secret</Text>
<Text fontSize="sm">Private Key</Text> </Flex>
</Flex> <Center
<Center w={isNotSmallerScreen ? '70%' : '100%'}
w={isNotSmallerScreen ? "70%" : "100%"} mt={isNotSmallerScreen ? '0' : '2'}
mt={isNotSmallerScreen ? "0" : "2"} >
> <InputField
<InputField borderRadius={5}
borderRadius={5} variables={variables}
variables={variables} setVariables={setVariables}
setVariables={setVariables} fieldVisibility={fieldVisibility}
inputType={TextAreaInputType.JWT_PRIVATE_KEY} setFieldVisibility={setFieldVisibility}
placeholder="Add private key here" inputType={HiddenInputType.JWT_SECRET}
minH="25vh" />
/> </Center>
</Center> </Flex>
</Flex> ) : (
</> <>
)} <Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
<Flex direction={isNotSmallerScreen ? "row" : "column"}> <Flex w="30%" justifyContent="start" alignItems="center">
<Flex <Text fontSize="sm">Public Key</Text>
w={isNotSmallerScreen ? "30%" : "40%"} </Flex>
justifyContent="start" <Center
alignItems="center" w={isNotSmallerScreen ? '70%' : '100%'}
> mt={isNotSmallerScreen ? '0' : '2'}
<Text fontSize="sm" orientation="vertical"> >
JWT Role Claim: <InputField
</Text> borderRadius={5}
</Flex> variables={variables}
<Center setVariables={setVariables}
w={isNotSmallerScreen ? "70%" : "100%"} inputType={TextAreaInputType.JWT_PUBLIC_KEY}
mt={isNotSmallerScreen ? "0" : "2"} placeholder="Add public key here"
> minH="25vh"
<InputField />
borderRadius={5} </Center>
variables={variables} </Flex>
setVariables={setVariables} <Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
inputType={TextInputType.JWT_ROLE_CLAIM} <Flex w="30%" justifyContent="start" alignItems="center">
/> <Text fontSize="sm">Private Key</Text>
</Center> </Flex>
</Flex> <Center
</Stack> w={isNotSmallerScreen ? '70%' : '100%'}
</div> mt={isNotSmallerScreen ? '0' : '2'}
); >
<InputField
borderRadius={5}
variables={variables}
setVariables={setVariables}
inputType={TextAreaInputType.JWT_PRIVATE_KEY}
placeholder="Add private key here"
minH="25vh"
/>
</Center>
</Flex>
</>
)}
<Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
<Flex
w={isNotSmallerScreen ? '30%' : '40%'}
justifyContent="start"
alignItems="center"
>
<Text fontSize="sm" orientation="vertical">
JWT Role Claim:
</Text>
</Flex>
<Center
w={isNotSmallerScreen ? '70%' : '100%'}
mt={isNotSmallerScreen ? '0' : '2'}
>
<InputField
borderRadius={5}
variables={variables}
setVariables={setVariables}
inputType={TextInputType.JWT_ROLE_CLAIM}
/>
</Center>
</Flex>
</Stack>
</div>
);
}; };
export default JSTConfigurations; export default JSTConfigurations;

View File

@@ -15,6 +15,7 @@ import {
FaFacebookF, FaFacebookF,
FaLinkedin, FaLinkedin,
FaApple, FaApple,
FaTwitter,
} from 'react-icons/fa'; } from 'react-icons/fa';
import { TextInputType, HiddenInputType } from '../../constants'; import { TextInputType, HiddenInputType } from '../../constants';
@@ -264,6 +265,44 @@ const OAuthConfig = ({
/> />
</Center> </Center>
</Flex> </Flex>
<Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
<Center
w={isNotSmallerScreen ? '55px' : '35px'}
h="35px"
marginRight="1.5%"
border="1px solid #3b5998"
borderRadius="5px"
>
<FaTwitter />
</Center>
<Center
w={isNotSmallerScreen ? '70%' : '100%'}
mt={isNotSmallerScreen ? '0' : '3'}
marginRight="1.5%"
>
<InputField
borderRadius={5}
variables={envVariables}
setVariables={setVariables}
inputType={TextInputType.TWITTER_CLIENT_ID}
placeholder="Twitter Client ID"
/>
</Center>
<Center
w={isNotSmallerScreen ? '70%' : '100%'}
mt={isNotSmallerScreen ? '0' : '3'}
>
<InputField
borderRadius={5}
variables={envVariables}
setVariables={setVariables}
fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.TWITTER_CLIENT_SECRET}
placeholder="Twitter Client Secret"
/>
</Center>
</Flex>
</Stack> </Stack>
</Box> </Box>
</div> </div>

View File

@@ -9,6 +9,7 @@ export const TextInputType = {
FACEBOOK_CLIENT_ID: 'FACEBOOK_CLIENT_ID', FACEBOOK_CLIENT_ID: 'FACEBOOK_CLIENT_ID',
LINKEDIN_CLIENT_ID: 'LINKEDIN_CLIENT_ID', LINKEDIN_CLIENT_ID: 'LINKEDIN_CLIENT_ID',
APPLE_CLIENT_ID: 'APPLE_CLIENT_ID', APPLE_CLIENT_ID: 'APPLE_CLIENT_ID',
TWITTER_CLIENT_ID: 'TWITTER_CLIENT_ID',
JWT_ROLE_CLAIM: 'JWT_ROLE_CLAIM', JWT_ROLE_CLAIM: 'JWT_ROLE_CLAIM',
REDIS_URL: 'REDIS_URL', REDIS_URL: 'REDIS_URL',
SMTP_HOST: 'SMTP_HOST', SMTP_HOST: 'SMTP_HOST',
@@ -35,6 +36,7 @@ export const HiddenInputType = {
FACEBOOK_CLIENT_SECRET: 'FACEBOOK_CLIENT_SECRET', FACEBOOK_CLIENT_SECRET: 'FACEBOOK_CLIENT_SECRET',
LINKEDIN_CLIENT_SECRET: 'LINKEDIN_CLIENT_SECRET', LINKEDIN_CLIENT_SECRET: 'LINKEDIN_CLIENT_SECRET',
APPLE_CLIENT_SECRET: 'APPLE_CLIENT_SECRET', APPLE_CLIENT_SECRET: 'APPLE_CLIENT_SECRET',
TWITTER_CLIENT_SECRET: 'TWITTER_CLIENT_SECRET',
JWT_SECRET: 'JWT_SECRET', JWT_SECRET: 'JWT_SECRET',
SMTP_PASSWORD: 'SMTP_PASSWORD', SMTP_PASSWORD: 'SMTP_PASSWORD',
ADMIN_SECRET: 'ADMIN_SECRET', ADMIN_SECRET: 'ADMIN_SECRET',
@@ -110,6 +112,8 @@ export interface envVarTypes {
LINKEDIN_CLIENT_SECRET: string; LINKEDIN_CLIENT_SECRET: string;
APPLE_CLIENT_ID: string; APPLE_CLIENT_ID: string;
APPLE_CLIENT_SECRET: string; APPLE_CLIENT_SECRET: string;
TWITTER_CLIENT_ID: string;
TWITTER_CLIENT_SECRET: string;
ROLES: [string] | []; ROLES: [string] | [];
DEFAULT_ROLES: [string] | []; DEFAULT_ROLES: [string] | [];
PROTECTED_ROLES: [string] | []; PROTECTED_ROLES: [string] | [];

View File

@@ -30,6 +30,8 @@ export const EnvVariablesQuery = `
LINKEDIN_CLIENT_SECRET LINKEDIN_CLIENT_SECRET
APPLE_CLIENT_ID APPLE_CLIENT_ID
APPLE_CLIENT_SECRET APPLE_CLIENT_SECRET
TWITTER_CLIENT_ID
TWITTER_CLIENT_SECRET
DEFAULT_ROLES DEFAULT_ROLES
PROTECTED_ROLES PROTECTED_ROLES
ROLES ROLES

View File

@@ -50,6 +50,8 @@ const Environment = () => {
LINKEDIN_CLIENT_SECRET: '', LINKEDIN_CLIENT_SECRET: '',
APPLE_CLIENT_ID: '', APPLE_CLIENT_ID: '',
APPLE_CLIENT_SECRET: '', APPLE_CLIENT_SECRET: '',
TWITTER_CLIENT_ID: '',
TWITTER_CLIENT_SECRET: '',
ROLES: [], ROLES: [],
DEFAULT_ROLES: [], DEFAULT_ROLES: [],
PROTECTED_ROLES: [], PROTECTED_ROLES: [],
@@ -92,6 +94,7 @@ const Environment = () => {
FACEBOOK_CLIENT_SECRET: false, FACEBOOK_CLIENT_SECRET: false,
LINKEDIN_CLIENT_SECRET: false, LINKEDIN_CLIENT_SECRET: false,
APPLE_CLIENT_SECRET: false, APPLE_CLIENT_SECRET: false,
TWITTER_CLIENT_SECRET: false,
JWT_SECRET: false, JWT_SECRET: false,
SMTP_PASSWORD: false, SMTP_PASSWORD: false,
ADMIN_SECRET: false, ADMIN_SECRET: false,

View File

@@ -29,6 +29,7 @@ import {
MenuItem, MenuItem,
useToast, useToast,
Spinner, Spinner,
TableContainer
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { import {
FaAngleLeft, FaAngleLeft,
@@ -262,9 +263,8 @@ export default function Users() {
.toPromise(); .toPromise();
if (res.data?._update_user?.id) { if (res.data?._update_user?.id) {
toast({ toast({
title: `Multi factor authentication ${ title: `Multi factor authentication ${user.is_multi_factor_auth_enabled ? 'disabled' : 'enabled'
user.is_multi_factor_auth_enabled ? 'disabled' : 'enabled' } for user`,
} for user`,
isClosable: true, isClosable: true,
status: 'success', status: 'success',
position: 'bottom-right', position: 'bottom-right',
@@ -293,260 +293,262 @@ export default function Users() {
</Flex> </Flex>
{!loading ? ( {!loading ? (
userList.length > 0 ? ( userList.length > 0 ? (
<Table variant="simple"> <TableContainer>
<Thead> <Table variant="simple">
<Tr> <Thead>
<Th>Email</Th> <Tr>
<Th>Created At</Th> <Th>Email</Th>
<Th>Signup Methods</Th> <Th>Created At</Th>
<Th>Roles</Th> <Th>Signup Methods</Th>
<Th>Verified</Th> <Th>Roles</Th>
<Th>Access</Th> <Th>Verified</Th>
<Th> <Th>Access</Th>
<Tooltip label="MultiFactor Authentication Enabled / Disabled"> <Th>
MFA <Tooltip label="MultiFactor Authentication Enabled / Disabled">
</Tooltip> MFA
</Th> </Tooltip>
<Th>Actions</Th> </Th>
</Tr> <Th>Actions</Th>
</Thead> </Tr>
<Tbody> </Thead>
{userList.map((user: userDataTypes) => { <Tbody>
const { email_verified, created_at, ...rest }: any = user; {userList.map((user: userDataTypes) => {
return ( const { email_verified, created_at, ...rest }: any = user;
<Tr key={user.id} style={{ fontSize: 14 }}> return (
<Td maxW="300">{user.email}</Td> <Tr key={user.id} style={{ fontSize: 14 }}>
<Td> <Td maxW="300">{user.email}</Td>
{dayjs(user.created_at * 1000).format('MMM DD, YYYY')} <Td>
</Td> {dayjs(user.created_at * 1000).format('MMM DD, YYYY')}
<Td>{user.signup_methods}</Td> </Td>
<Td>{user.roles.join(', ')}</Td> <Td>{user.signup_methods}</Td>
<Td> <Td>{user.roles.join(', ')}</Td>
<Tag <Td>
size="sm" <Tag
variant="outline" size="sm"
colorScheme={user.email_verified ? 'green' : 'yellow'} variant="outline"
> colorScheme={user.email_verified ? 'green' : 'yellow'}
{user.email_verified.toString()} >
</Tag> {user.email_verified.toString()}
</Td> </Tag>
<Td> </Td>
<Tag <Td>
size="sm" <Tag
variant="outline" size="sm"
colorScheme={user.revoked_timestamp ? 'red' : 'green'} variant="outline"
> colorScheme={user.revoked_timestamp ? 'red' : 'green'}
{user.revoked_timestamp ? 'Revoked' : 'Enabled'} >
</Tag> {user.revoked_timestamp ? 'Revoked' : 'Enabled'}
</Td> </Tag>
<Td> </Td>
<Tag <Td>
size="sm" <Tag
variant="outline" size="sm"
colorScheme={ variant="outline"
user.is_multi_factor_auth_enabled ? 'green' : 'red' colorScheme={
} user.is_multi_factor_auth_enabled ? 'green' : 'red'
> }
{user.is_multi_factor_auth_enabled >
? 'Enabled' {user.is_multi_factor_auth_enabled
: 'Disabled'} ? 'Enabled'
</Tag> : 'Disabled'}
</Td> </Tag>
<Td> </Td>
<Menu> <Td>
<MenuButton as={Button} variant="unstyled" size="sm"> <Menu>
<Flex <MenuButton as={Button} variant="unstyled" size="sm">
justifyContent="space-between" <Flex
alignItems="center" justifyContent="space-between"
> alignItems="center"
<Text fontSize="sm" fontWeight="light">
Menu
</Text>
<FaAngleDown style={{ marginLeft: 10 }} />
</Flex>
</MenuButton>
<MenuList>
{!user.email_verified && (
<MenuItem
onClick={() => userVerificationHandler(user)}
> >
Verify User <Text fontSize="sm" fontWeight="light">
</MenuItem> Menu
)} </Text>
<EditUserModal <FaAngleDown style={{ marginLeft: 10 }} />
user={rest} </Flex>
updateUserList={updateUserList} </MenuButton>
/> <MenuList>
<DeleteUserModal {!user.email_verified && (
user={rest} <MenuItem
updateUserList={updateUserList} onClick={() => userVerificationHandler(user)}
/> >
{user.revoked_timestamp ? ( Verify User
<MenuItem </MenuItem>
onClick={() => )}
updateAccessHandler( <EditUserModal
user.id, user={rest}
updateAccessActions.ENABLE updateUserList={updateUserList}
) />
} <DeleteUserModal
> user={rest}
Enable Access updateUserList={updateUserList}
</MenuItem> />
) : ( {user.revoked_timestamp ? (
<MenuItem <MenuItem
onClick={() => onClick={() =>
updateAccessHandler( updateAccessHandler(
user.id, user.id,
updateAccessActions.REVOKE updateAccessActions.ENABLE
) )
} }
> >
Revoke Access Enable Access
</MenuItem> </MenuItem>
)} ) : (
{user.is_multi_factor_auth_enabled ? ( <MenuItem
<MenuItem onClick={() =>
onClick={() => multiFactorAuthUpdateHandler(user)} updateAccessHandler(
> user.id,
Disable MultiFactor Authentication updateAccessActions.REVOKE
</MenuItem> )
) : ( }
<MenuItem >
onClick={() => multiFactorAuthUpdateHandler(user)} Revoke Access
> </MenuItem>
Enable MultiFactor Authentication )}
</MenuItem> {user.is_multi_factor_auth_enabled ? (
)} <MenuItem
</MenuList> onClick={() => multiFactorAuthUpdateHandler(user)}
</Menu> >
</Td> Disable MultiFactor Authentication
</Tr> </MenuItem>
); ) : (
})} <MenuItem
</Tbody> onClick={() => multiFactorAuthUpdateHandler(user)}
{(paginationProps.maxPages > 1 || paginationProps.total >= 5) && ( >
<TableCaption> Enable MultiFactor Authentication
<Flex </MenuItem>
justifyContent="space-between" )}
alignItems="center" </MenuList>
m="2% 0" </Menu>
> </Td>
<Flex flex="1"> </Tr>
<Tooltip label="First Page"> );
<IconButton })}
aria-label="icon button" </Tbody>
onClick={() => {(paginationProps.maxPages > 1 || paginationProps.total >= 5) && (
<TableCaption>
<Flex
justifyContent="space-between"
alignItems="center"
m="2% 0"
>
<Flex flex="1">
<Tooltip label="First Page">
<IconButton
aria-label="icon button"
onClick={() =>
paginationHandler({
page: 1,
})
}
isDisabled={paginationProps.page <= 1}
mr={4}
icon={<FaAngleDoubleLeft />}
/>
</Tooltip>
<Tooltip label="Previous Page">
<IconButton
aria-label="icon button"
onClick={() =>
paginationHandler({
page: paginationProps.page - 1,
})
}
isDisabled={paginationProps.page <= 1}
icon={<FaAngleLeft />}
/>
</Tooltip>
</Flex>
<Flex
flex="8"
justifyContent="space-evenly"
alignItems="center"
>
<Text mr={8}>
Page{' '}
<Text fontWeight="bold" as="span">
{paginationProps.page}
</Text>{' '}
of{' '}
<Text fontWeight="bold" as="span">
{paginationProps.maxPages}
</Text>
</Text>
<Flex alignItems="center">
<Text flexShrink="0">Go to page:</Text>{' '}
<NumberInput
ml={2}
mr={8}
w={28}
min={1}
max={paginationProps.maxPages}
onChange={(value) =>
paginationHandler({
page: parseInt(value),
})
}
value={paginationProps.page}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</Flex>
<Select
w={32}
value={paginationProps.limit}
onChange={(e) =>
paginationHandler({ paginationHandler({
page: 1, page: 1,
limit: parseInt(e.target.value),
}) })
} }
isDisabled={paginationProps.page <= 1}
mr={4}
icon={<FaAngleDoubleLeft />}
/>
</Tooltip>
<Tooltip label="Previous Page">
<IconButton
aria-label="icon button"
onClick={() =>
paginationHandler({
page: paginationProps.page - 1,
})
}
isDisabled={paginationProps.page <= 1}
icon={<FaAngleLeft />}
/>
</Tooltip>
</Flex>
<Flex
flex="8"
justifyContent="space-evenly"
alignItems="center"
>
<Text mr={8}>
Page{' '}
<Text fontWeight="bold" as="span">
{paginationProps.page}
</Text>{' '}
of{' '}
<Text fontWeight="bold" as="span">
{paginationProps.maxPages}
</Text>
</Text>
<Flex alignItems="center">
<Text flexShrink="0">Go to page:</Text>{' '}
<NumberInput
ml={2}
mr={8}
w={28}
min={1}
max={paginationProps.maxPages}
onChange={(value) =>
paginationHandler({
page: parseInt(value),
})
}
value={paginationProps.page}
> >
<NumberInputField /> {getLimits(paginationProps).map((pageSize) => (
<NumberInputStepper> <option key={pageSize} value={pageSize}>
<NumberIncrementStepper /> Show {pageSize}
<NumberDecrementStepper /> </option>
</NumberInputStepper> ))}
</NumberInput> </Select>
</Flex>
<Flex flex="1">
<Tooltip label="Next Page">
<IconButton
aria-label="icon button"
onClick={() =>
paginationHandler({
page: paginationProps.page + 1,
})
}
isDisabled={
paginationProps.page >= paginationProps.maxPages
}
icon={<FaAngleRight />}
/>
</Tooltip>
<Tooltip label="Last Page">
<IconButton
aria-label="icon button"
onClick={() =>
paginationHandler({
page: paginationProps.maxPages,
})
}
isDisabled={
paginationProps.page >= paginationProps.maxPages
}
ml={4}
icon={<FaAngleDoubleRight />}
/>
</Tooltip>
</Flex> </Flex>
<Select
w={32}
value={paginationProps.limit}
onChange={(e) =>
paginationHandler({
page: 1,
limit: parseInt(e.target.value),
})
}
>
{getLimits(paginationProps).map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</Select>
</Flex> </Flex>
<Flex flex="1"> </TableCaption>
<Tooltip label="Next Page"> )}
<IconButton </Table>
aria-label="icon button" </TableContainer>
onClick={() =>
paginationHandler({
page: paginationProps.page + 1,
})
}
isDisabled={
paginationProps.page >= paginationProps.maxPages
}
icon={<FaAngleRight />}
/>
</Tooltip>
<Tooltip label="Last Page">
<IconButton
aria-label="icon button"
onClick={() =>
paginationHandler({
page: paginationProps.maxPages,
})
}
isDisabled={
paginationProps.page >= paginationProps.maxPages
}
ml={4}
icon={<FaAngleDoubleRight />}
/>
</Tooltip>
</Flex>
</Flex>
</TableCaption>
)}
</Table>
) : ( ) : (
<Flex <Flex
flexDirection="column" flexDirection="column"

View File

@@ -29,19 +29,16 @@ const fallbackCopyTextToClipboard = (text: string) => {
document.body.removeChild(textArea); document.body.removeChild(textArea);
}; };
export const copyTextToClipboard = (text: string) => { export const copyTextToClipboard = async (text: string) => {
if (!navigator.clipboard) { if (!navigator.clipboard) {
fallbackCopyTextToClipboard(text); fallbackCopyTextToClipboard(text);
return; return;
} }
navigator.clipboard.writeText(text).then( try {
() => { navigator.clipboard.writeText(text);
console.log('Async: Copying to clipboard was successful!'); } catch (err) {
}, throw err;
(err) => { }
console.error('Async: Could not copy text: ', err);
}
);
}; };
export const getObjectDiff = (obj1: any, obj2: any) => { export const getObjectDiff = (obj1: any, obj2: any) => {

View File

@@ -15,4 +15,6 @@ const (
AuthRecipeMethodLinkedIn = "linkedin" AuthRecipeMethodLinkedIn = "linkedin"
// AuthRecipeMethodApple is the apple auth method // AuthRecipeMethodApple is the apple auth method
AuthRecipeMethodApple = "apple" AuthRecipeMethodApple = "apple"
// AuthRecipeMethodTwitter is the twitter auth method
AuthRecipeMethodTwitter = "twitter"
) )

View File

@@ -49,6 +49,10 @@ const (
EnvKeySenderEmail = "SENDER_EMAIL" EnvKeySenderEmail = "SENDER_EMAIL"
// EnvKeyIsEmailServiceEnabled key for env variable IS_EMAIL_SERVICE_ENABLED // EnvKeyIsEmailServiceEnabled key for env variable IS_EMAIL_SERVICE_ENABLED
EnvKeyIsEmailServiceEnabled = "IS_EMAIL_SERVICE_ENABLED" EnvKeyIsEmailServiceEnabled = "IS_EMAIL_SERVICE_ENABLED"
// EnvKeyAppCookieSecure key for env variable APP_COOKIE_SECURE
EnvKeyAppCookieSecure = "APP_COOKIE_SECURE"
// EnvKeyAdminCookieSecure key for env variable ADMIN_COOKIE_SECURE
EnvKeyAdminCookieSecure = "ADMIN_COOKIE_SECURE"
// EnvKeyJwtType key for env variable JWT_TYPE // EnvKeyJwtType key for env variable JWT_TYPE
EnvKeyJwtType = "JWT_TYPE" EnvKeyJwtType = "JWT_TYPE"
// EnvKeyJwtSecret key for env variable JWT_SECRET // EnvKeyJwtSecret key for env variable JWT_SECRET
@@ -85,6 +89,10 @@ const (
EnvKeyAppleClientID = "APPLE_CLIENT_ID" EnvKeyAppleClientID = "APPLE_CLIENT_ID"
// EnvKeyAppleClientSecret key for env variable APPLE_CLIENT_SECRET // EnvKeyAppleClientSecret key for env variable APPLE_CLIENT_SECRET
EnvKeyAppleClientSecret = "APPLE_CLIENT_SECRET" EnvKeyAppleClientSecret = "APPLE_CLIENT_SECRET"
// EnvKeyTwitterClientID key for env variable TWITTER_CLIENT_ID
EnvKeyTwitterClientID = "TWITTER_CLIENT_ID"
// EnvKeyTwitterClientSecret key for env variable TWITTER_CLIENT_SECRET
EnvKeyTwitterClientSecret = "TWITTER_CLIENT_SECRET"
// EnvKeyOrganizationName key for env variable ORGANIZATION_NAME // EnvKeyOrganizationName key for env variable ORGANIZATION_NAME
EnvKeyOrganizationName = "ORGANIZATION_NAME" EnvKeyOrganizationName = "ORGANIZATION_NAME"
// EnvKeyOrganizationLogo key for env variable ORGANIZATION_LOGO // EnvKeyOrganizationLogo key for env variable ORGANIZATION_LOGO

View File

@@ -9,9 +9,11 @@ const (
// Ref: https://docs.github.com/en/developers/apps/building-github-apps/identifying-and-authorizing-users-for-github-apps#3-your-github-app-accesses-the-api-with-the-users-access-token // Ref: https://docs.github.com/en/developers/apps/building-github-apps/identifying-and-authorizing-users-for-github-apps#3-your-github-app-accesses-the-api-with-the-users-access-token
GithubUserInfoURL = "https://api.github.com/user" GithubUserInfoURL = "https://api.github.com/user"
// Get github user emails when user info email is empty Ref: https://stackoverflow.com/a/35387123 // Get github user emails when user info email is empty Ref: https://stackoverflow.com/a/35387123
GithubUserEmails = "https://api/github.com/user/emails" GithubUserEmails = "https://api.github.com/user/emails"
// Ref: https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api // Ref: https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api
LinkedInUserInfoURL = "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,emailAddress,profilePicture(displayImage~:playableStreams))" LinkedInUserInfoURL = "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,emailAddress,profilePicture(displayImage~:playableStreams))"
LinkedInEmailURL = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))" LinkedInEmailURL = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))"
TwitterUserInfoURL = "https://api.twitter.com/2/users/me?user.fields=id,name,profile_image_url,username"
) )

View File

@@ -14,3 +14,14 @@ const (
// VerificationTypeOTP is the otp verification type // VerificationTypeOTP is the otp verification type
VerificationTypeOTP = "verify_otp" VerificationTypeOTP = "verify_otp"
) )
var (
// VerificationTypes is slice of all verification types
VerificationTypes = []string{
VerificationTypeBasicAuthSignup,
VerificationTypeMagicLinkLogin,
VerificationTypeUpdateEmail,
VerificationTypeForgotPassword,
VerificationTypeInviteMember,
}
)

View File

@@ -3,15 +3,24 @@ package cookie
import ( import (
"net/url" "net/url"
log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// SetAdminCookie sets the admin cookie in the response // SetAdminCookie sets the admin cookie in the response
func SetAdminCookie(gc *gin.Context, token string) { func SetAdminCookie(gc *gin.Context, token string) {
secure := true adminCookieSecure, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyAdminCookieSecure)
httpOnly := true if err != nil {
log.Debug("Error while getting admin cookie secure from env variable: %v", err)
adminCookieSecure = true
}
secure := adminCookieSecure
httpOnly := adminCookieSecure
hostname := parsers.GetHost(gc) hostname := parsers.GetHost(gc)
host, _ := parsers.GetHostParts(hostname) host, _ := parsers.GetHostParts(hostname)
gc.SetCookie(constants.AdminCookieName, token, 3600, "/", host, secure, httpOnly) gc.SetCookie(constants.AdminCookieName, token, 3600, "/", host, secure, httpOnly)
@@ -35,8 +44,14 @@ func GetAdminCookie(gc *gin.Context) (string, error) {
// DeleteAdminCookie sets the response cookie to empty // DeleteAdminCookie sets the response cookie to empty
func DeleteAdminCookie(gc *gin.Context) { func DeleteAdminCookie(gc *gin.Context) {
secure := true adminCookieSecure, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyAdminCookieSecure)
httpOnly := true if err != nil {
log.Debug("Error while getting admin cookie secure from env variable: %v", err)
adminCookieSecure = true
}
secure := adminCookieSecure
httpOnly := adminCookieSecure
hostname := parsers.GetHost(gc) hostname := parsers.GetHost(gc)
host, _ := parsers.GetHostParts(hostname) host, _ := parsers.GetHostParts(hostname)
gc.SetCookie(constants.AdminCookieName, "", -1, "/", host, secure, httpOnly) gc.SetCookie(constants.AdminCookieName, "", -1, "/", host, secure, httpOnly)

View File

@@ -4,15 +4,24 @@ import (
"net/http" "net/http"
"net/url" "net/url"
log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// SetSession sets the session cookie in the response // SetSession sets the session cookie in the response
func SetSession(gc *gin.Context, sessionID string) { func SetSession(gc *gin.Context, sessionID string) {
secure := true appCookieSecure, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyAppCookieSecure)
httpOnly := true if err != nil {
log.Debug("Error while getting app cookie secure from env variable: %v", err)
appCookieSecure = true
}
secure := appCookieSecure
httpOnly := appCookieSecure
hostname := parsers.GetHost(gc) hostname := parsers.GetHost(gc)
host, _ := parsers.GetHostParts(hostname) host, _ := parsers.GetHostParts(hostname)
domain := parsers.GetDomainName(hostname) domain := parsers.GetDomainName(hostname)
@@ -20,18 +29,35 @@ func SetSession(gc *gin.Context, sessionID string) {
domain = "." + domain domain = "." + domain
} }
// Use sameSite = lax by default
// Since app cookie can come from cross site it becomes important to set this in lax mode.
// Example person using custom UI on their app domain and making request to authorizer domain.
// For more information check:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
// https://github.com/gin-gonic/gin/blob/master/context.go#L86
// TODO add ability to sameSite = none / strict from dashboard
if !appCookieSecure {
gc.SetSameSite(http.SameSiteLaxMode)
} else {
gc.SetSameSite(http.SameSiteNoneMode)
}
// TODO allow configuring from dashboard // TODO allow configuring from dashboard
year := 60 * 60 * 24 * 365 year := 60 * 60 * 24 * 365
gc.SetSameSite(http.SameSiteNoneMode)
gc.SetCookie(constants.AppCookieName+"_session", sessionID, year, "/", host, secure, httpOnly) gc.SetCookie(constants.AppCookieName+"_session", sessionID, year, "/", host, secure, httpOnly)
gc.SetCookie(constants.AppCookieName+"_session_domain", sessionID, year, "/", domain, secure, httpOnly) gc.SetCookie(constants.AppCookieName+"_session_domain", sessionID, year, "/", domain, secure, httpOnly)
} }
// DeleteSession sets session cookies to expire // DeleteSession sets session cookies to expire
func DeleteSession(gc *gin.Context) { func DeleteSession(gc *gin.Context) {
secure := true appCookieSecure, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyAppCookieSecure)
httpOnly := true if err != nil {
log.Debug("Error while getting app cookie secure from env variable: %v", err)
appCookieSecure = true
}
secure := appCookieSecure
httpOnly := appCookieSecure
hostname := parsers.GetHost(gc) hostname := parsers.GetHost(gc)
host, _ := parsers.GetHostParts(hostname) host, _ := parsers.GetHostParts(hostname)
domain := parsers.GetDomainName(hostname) domain := parsers.GetDomainName(hostname)

View File

@@ -1 +0,0 @@
package email

60
server/env/env.go vendored
View File

@@ -72,11 +72,15 @@ func InitAllEnv() error {
osLinkedInClientSecret := os.Getenv(constants.EnvKeyLinkedInClientSecret) osLinkedInClientSecret := os.Getenv(constants.EnvKeyLinkedInClientSecret)
osAppleClientID := os.Getenv(constants.EnvKeyAppleClientID) osAppleClientID := os.Getenv(constants.EnvKeyAppleClientID)
osAppleClientSecret := os.Getenv(constants.EnvKeyAppleClientSecret) osAppleClientSecret := os.Getenv(constants.EnvKeyAppleClientSecret)
osTwitterClientID := os.Getenv(constants.EnvKeyTwitterClientID)
osTwitterClientSecret := os.Getenv(constants.EnvKeyTwitterClientSecret)
osResetPasswordURL := os.Getenv(constants.EnvKeyResetPasswordURL) osResetPasswordURL := os.Getenv(constants.EnvKeyResetPasswordURL)
osOrganizationName := os.Getenv(constants.EnvKeyOrganizationName) osOrganizationName := os.Getenv(constants.EnvKeyOrganizationName)
osOrganizationLogo := os.Getenv(constants.EnvKeyOrganizationLogo) osOrganizationLogo := os.Getenv(constants.EnvKeyOrganizationLogo)
// os bool vars // os bool vars
osAppCookieSecure := os.Getenv(constants.EnvKeyAppCookieSecure)
osAdminCookieSecure := os.Getenv(constants.EnvKeyAdminCookieSecure)
osDisableBasicAuthentication := os.Getenv(constants.EnvKeyDisableBasicAuthentication) osDisableBasicAuthentication := os.Getenv(constants.EnvKeyDisableBasicAuthentication)
osDisableEmailVerification := os.Getenv(constants.EnvKeyDisableEmailVerification) osDisableEmailVerification := os.Getenv(constants.EnvKeyDisableEmailVerification)
osDisableMagicLinkLogin := os.Getenv(constants.EnvKeyDisableMagicLinkLogin) osDisableMagicLinkLogin := os.Getenv(constants.EnvKeyDisableMagicLinkLogin)
@@ -355,31 +359,45 @@ func InitAllEnv() error {
if val, ok := envData[constants.EnvKeyLinkedInClientID]; !ok || val == "" { if val, ok := envData[constants.EnvKeyLinkedInClientID]; !ok || val == "" {
envData[constants.EnvKeyLinkedInClientID] = osLinkedInClientID envData[constants.EnvKeyLinkedInClientID] = osLinkedInClientID
} }
if osFacebookClientID != "" && envData[constants.EnvKeyLinkedInClientID] != osFacebookClientID { if osLinkedInClientID != "" && envData[constants.EnvKeyLinkedInClientID] != osLinkedInClientID {
envData[constants.EnvKeyLinkedInClientID] = osLinkedInClientID envData[constants.EnvKeyLinkedInClientID] = osLinkedInClientID
} }
if val, ok := envData[constants.EnvKeyLinkedInClientSecret]; !ok || val == "" { if val, ok := envData[constants.EnvKeyLinkedInClientSecret]; !ok || val == "" {
envData[constants.EnvKeyLinkedInClientSecret] = osLinkedInClientSecret envData[constants.EnvKeyLinkedInClientSecret] = osLinkedInClientSecret
} }
if osFacebookClientSecret != "" && envData[constants.EnvKeyLinkedInClientSecret] != osFacebookClientSecret { if osLinkedInClientSecret != "" && envData[constants.EnvKeyLinkedInClientSecret] != osLinkedInClientSecret {
envData[constants.EnvKeyLinkedInClientSecret] = osLinkedInClientSecret envData[constants.EnvKeyLinkedInClientSecret] = osLinkedInClientSecret
} }
if val, ok := envData[constants.EnvKeyAppleClientID]; !ok || val == "" { if val, ok := envData[constants.EnvKeyAppleClientID]; !ok || val == "" {
envData[constants.EnvKeyAppleClientID] = osAppleClientID envData[constants.EnvKeyAppleClientID] = osAppleClientID
} }
if osFacebookClientID != "" && envData[constants.EnvKeyAppleClientID] != osFacebookClientID { if osAppleClientID != "" && envData[constants.EnvKeyAppleClientID] != osAppleClientID {
envData[constants.EnvKeyAppleClientID] = osAppleClientID envData[constants.EnvKeyAppleClientID] = osAppleClientID
} }
if val, ok := envData[constants.EnvKeyAppleClientSecret]; !ok || val == "" { if val, ok := envData[constants.EnvKeyAppleClientSecret]; !ok || val == "" {
envData[constants.EnvKeyAppleClientSecret] = osAppleClientSecret envData[constants.EnvKeyAppleClientSecret] = osAppleClientSecret
} }
if osFacebookClientSecret != "" && envData[constants.EnvKeyAppleClientSecret] != osFacebookClientSecret { if osAppleClientSecret != "" && envData[constants.EnvKeyAppleClientSecret] != osAppleClientSecret {
envData[constants.EnvKeyAppleClientSecret] = osAppleClientSecret envData[constants.EnvKeyAppleClientSecret] = osAppleClientSecret
} }
if val, ok := envData[constants.EnvKeyTwitterClientID]; !ok || val == "" {
envData[constants.EnvKeyTwitterClientID] = osTwitterClientID
}
if osTwitterClientID != "" && envData[constants.EnvKeyTwitterClientID] != osTwitterClientID {
envData[constants.EnvKeyTwitterClientID] = osTwitterClientID
}
if val, ok := envData[constants.EnvKeyTwitterClientSecret]; !ok || val == "" {
envData[constants.EnvKeyTwitterClientSecret] = osTwitterClientSecret
}
if osTwitterClientSecret != "" && envData[constants.EnvKeyTwitterClientSecret] != osTwitterClientSecret {
envData[constants.EnvKeyTwitterClientSecret] = osTwitterClientSecret
}
if val, ok := envData[constants.EnvKeyResetPasswordURL]; !ok || val == "" { if val, ok := envData[constants.EnvKeyResetPasswordURL]; !ok || val == "" {
envData[constants.EnvKeyResetPasswordURL] = strings.TrimPrefix(osResetPasswordURL, "/") envData[constants.EnvKeyResetPasswordURL] = strings.TrimPrefix(osResetPasswordURL, "/")
} }
@@ -401,6 +419,40 @@ func InitAllEnv() error {
envData[constants.EnvKeyOrganizationLogo] = osOrganizationLogo envData[constants.EnvKeyOrganizationLogo] = osOrganizationLogo
} }
if _, ok := envData[constants.EnvKeyAppCookieSecure]; !ok {
if osAppCookieSecure == "" {
envData[constants.EnvKeyAppCookieSecure] = true
} else {
envData[constants.EnvKeyAppCookieSecure] = osAppCookieSecure == "true"
}
}
if osAppCookieSecure != "" {
boolValue, err := strconv.ParseBool(osAppCookieSecure)
if err != nil {
return err
}
if boolValue != envData[constants.EnvKeyAppCookieSecure].(bool) {
envData[constants.EnvKeyAppCookieSecure] = boolValue
}
}
if _, ok := envData[constants.EnvKeyAdminCookieSecure]; !ok {
if osAdminCookieSecure == "" {
envData[constants.EnvKeyAdminCookieSecure] = true
} else {
envData[constants.EnvKeyAdminCookieSecure] = osAdminCookieSecure == "true"
}
}
if osAdminCookieSecure != "" {
boolValue, err := strconv.ParseBool(osAdminCookieSecure)
if err != nil {
return err
}
if boolValue != envData[constants.EnvKeyAdminCookieSecure].(bool) {
envData[constants.EnvKeyAdminCookieSecure] = boolValue
}
}
if _, ok := envData[constants.EnvKeyDisableBasicAuthentication]; !ok { if _, ok := envData[constants.EnvKeyDisableBasicAuthentication]; !ok {
envData[constants.EnvKeyDisableBasicAuthentication] = osDisableBasicAuthentication == "true" envData[constants.EnvKeyDisableBasicAuthentication] = osDisableBasicAuthentication == "true"
} }

View File

@@ -201,7 +201,7 @@ func PersistEnv() error {
envValue := strings.TrimSpace(os.Getenv(key)) envValue := strings.TrimSpace(os.Getenv(key))
if envValue != "" { if envValue != "" {
switch key { switch key {
case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled, constants.EnvKeyEnforceMultiFactorAuthentication, constants.EnvKeyDisableMultiFactorAuthentication: case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled, constants.EnvKeyEnforceMultiFactorAuthentication, constants.EnvKeyDisableMultiFactorAuthentication, constants.EnvKeyAdminCookieSecure, constants.EnvKeyAppCookieSecure:
if envValueBool, err := strconv.ParseBool(envValue); err == nil { if envValueBool, err := strconv.ParseBool(envValue); err == nil {
if value.(bool) != envValueBool { if value.(bool) != envValueBool {
storeData[key] = envValueBool storeData[key] = envValueBool

View File

@@ -119,6 +119,8 @@ type ComplexityRoot struct {
SMTPPort func(childComplexity int) int SMTPPort func(childComplexity int) int
SMTPUsername func(childComplexity int) int SMTPUsername func(childComplexity int) int
SenderEmail func(childComplexity int) int SenderEmail func(childComplexity int) int
TwitterClientID func(childComplexity int) int
TwitterClientSecret func(childComplexity int) int
} }
Error struct { Error struct {
@@ -145,6 +147,7 @@ type ComplexityRoot struct {
IsMultiFactorAuthEnabled func(childComplexity int) int IsMultiFactorAuthEnabled func(childComplexity int) int
IsSignUpEnabled func(childComplexity int) int IsSignUpEnabled func(childComplexity int) int
IsStrongPasswordEnabled func(childComplexity int) int IsStrongPasswordEnabled func(childComplexity int) int
IsTwitterLoginEnabled func(childComplexity int) int
Version func(childComplexity int) int Version func(childComplexity int) int
} }
@@ -813,6 +816,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Env.SenderEmail(childComplexity), true return e.complexity.Env.SenderEmail(childComplexity), true
case "Env.TWITTER_CLIENT_ID":
if e.complexity.Env.TwitterClientID == nil {
break
}
return e.complexity.Env.TwitterClientID(childComplexity), true
case "Env.TWITTER_CLIENT_SECRET":
if e.complexity.Env.TwitterClientSecret == nil {
break
}
return e.complexity.Env.TwitterClientSecret(childComplexity), true
case "Error.message": case "Error.message":
if e.complexity.Error.Message == nil { if e.complexity.Error.Message == nil {
break break
@@ -932,6 +949,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Meta.IsStrongPasswordEnabled(childComplexity), true return e.complexity.Meta.IsStrongPasswordEnabled(childComplexity), true
case "Meta.is_twitter_login_enabled":
if e.complexity.Meta.IsTwitterLoginEnabled == nil {
break
}
return e.complexity.Meta.IsTwitterLoginEnabled(childComplexity), true
case "Meta.version": case "Meta.version":
if e.complexity.Meta.Version == nil { if e.complexity.Meta.Version == nil {
break break
@@ -1893,6 +1917,7 @@ type Meta {
is_github_login_enabled: Boolean! is_github_login_enabled: Boolean!
is_linkedin_login_enabled: Boolean! is_linkedin_login_enabled: Boolean!
is_apple_login_enabled: Boolean! is_apple_login_enabled: Boolean!
is_twitter_login_enabled: Boolean!
is_email_verification_enabled: Boolean! is_email_verification_enabled: Boolean!
is_basic_authentication_enabled: Boolean! is_basic_authentication_enabled: Boolean!
is_magic_link_login_enabled: Boolean! is_magic_link_login_enabled: Boolean!
@@ -2014,6 +2039,8 @@ type Env {
LINKEDIN_CLIENT_SECRET: String LINKEDIN_CLIENT_SECRET: String
APPLE_CLIENT_ID: String APPLE_CLIENT_ID: String
APPLE_CLIENT_SECRET: String APPLE_CLIENT_SECRET: String
TWITTER_CLIENT_ID: String
TWITTER_CLIENT_SECRET: String
ORGANIZATION_NAME: String ORGANIZATION_NAME: String
ORGANIZATION_LOGO: String ORGANIZATION_LOGO: String
} }
@@ -2118,6 +2145,8 @@ input UpdateEnvInput {
LINKEDIN_CLIENT_SECRET: String LINKEDIN_CLIENT_SECRET: String
APPLE_CLIENT_ID: String APPLE_CLIENT_ID: String
APPLE_CLIENT_SECRET: String APPLE_CLIENT_SECRET: String
TWITTER_CLIENT_ID: String
TWITTER_CLIENT_SECRET: String
ORGANIZATION_NAME: String ORGANIZATION_NAME: String
ORGANIZATION_LOGO: String ORGANIZATION_LOGO: String
} }
@@ -5054,6 +5083,70 @@ func (ec *executionContext) _Env_APPLE_CLIENT_SECRET(ctx context.Context, field
return ec.marshalOString2ᚖstring(ctx, field.Selections, res) return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
} }
func (ec *executionContext) _Env_TWITTER_CLIENT_ID(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Env",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.TwitterClientID, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _Env_TWITTER_CLIENT_SECRET(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Env",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.TwitterClientSecret, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _Env_ORGANIZATION_NAME(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { func (ec *executionContext) _Env_ORGANIZATION_NAME(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -5529,6 +5622,41 @@ func (ec *executionContext) _Meta_is_apple_login_enabled(ctx context.Context, fi
return ec.marshalNBoolean2bool(ctx, field.Selections, res) return ec.marshalNBoolean2bool(ctx, field.Selections, res)
} }
func (ec *executionContext) _Meta_is_twitter_login_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Meta",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.IsTwitterLoginEnabled, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _Meta_is_email_verification_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) { func (ec *executionContext) _Meta_is_email_verification_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -11720,6 +11848,22 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob
if err != nil { if err != nil {
return it, err return it, err
} }
case "TWITTER_CLIENT_ID":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("TWITTER_CLIENT_ID"))
it.TwitterClientID, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
case "TWITTER_CLIENT_SECRET":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("TWITTER_CLIENT_SECRET"))
it.TwitterClientSecret, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
case "ORGANIZATION_NAME": case "ORGANIZATION_NAME":
var err error var err error
@@ -12421,6 +12565,10 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj
out.Values[i] = ec._Env_APPLE_CLIENT_ID(ctx, field, obj) out.Values[i] = ec._Env_APPLE_CLIENT_ID(ctx, field, obj)
case "APPLE_CLIENT_SECRET": case "APPLE_CLIENT_SECRET":
out.Values[i] = ec._Env_APPLE_CLIENT_SECRET(ctx, field, obj) out.Values[i] = ec._Env_APPLE_CLIENT_SECRET(ctx, field, obj)
case "TWITTER_CLIENT_ID":
out.Values[i] = ec._Env_TWITTER_CLIENT_ID(ctx, field, obj)
case "TWITTER_CLIENT_SECRET":
out.Values[i] = ec._Env_TWITTER_CLIENT_SECRET(ctx, field, obj)
case "ORGANIZATION_NAME": case "ORGANIZATION_NAME":
out.Values[i] = ec._Env_ORGANIZATION_NAME(ctx, field, obj) out.Values[i] = ec._Env_ORGANIZATION_NAME(ctx, field, obj)
case "ORGANIZATION_LOGO": case "ORGANIZATION_LOGO":
@@ -12542,6 +12690,11 @@ func (ec *executionContext) _Meta(ctx context.Context, sel ast.SelectionSet, obj
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "is_twitter_login_enabled":
out.Values[i] = ec._Meta_is_twitter_login_enabled(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "is_email_verification_enabled": case "is_email_verification_enabled":
out.Values[i] = ec._Meta_is_email_verification_enabled(ctx, field, obj) out.Values[i] = ec._Meta_is_email_verification_enabled(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {

View File

@@ -106,6 +106,8 @@ type Env struct {
LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"` LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"`
AppleClientID *string `json:"APPLE_CLIENT_ID"` AppleClientID *string `json:"APPLE_CLIENT_ID"`
AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"` AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"`
TwitterClientID *string `json:"TWITTER_CLIENT_ID"`
TwitterClientSecret *string `json:"TWITTER_CLIENT_SECRET"`
OrganizationName *string `json:"ORGANIZATION_NAME"` OrganizationName *string `json:"ORGANIZATION_NAME"`
OrganizationLogo *string `json:"ORGANIZATION_LOGO"` OrganizationLogo *string `json:"ORGANIZATION_LOGO"`
} }
@@ -164,6 +166,7 @@ type Meta struct {
IsGithubLoginEnabled bool `json:"is_github_login_enabled"` IsGithubLoginEnabled bool `json:"is_github_login_enabled"`
IsLinkedinLoginEnabled bool `json:"is_linkedin_login_enabled"` IsLinkedinLoginEnabled bool `json:"is_linkedin_login_enabled"`
IsAppleLoginEnabled bool `json:"is_apple_login_enabled"` IsAppleLoginEnabled bool `json:"is_apple_login_enabled"`
IsTwitterLoginEnabled bool `json:"is_twitter_login_enabled"`
IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"` IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"`
IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"` IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"`
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"` IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
@@ -297,6 +300,8 @@ type UpdateEnvInput struct {
LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"` LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"`
AppleClientID *string `json:"APPLE_CLIENT_ID"` AppleClientID *string `json:"APPLE_CLIENT_ID"`
AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"` AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"`
TwitterClientID *string `json:"TWITTER_CLIENT_ID"`
TwitterClientSecret *string `json:"TWITTER_CLIENT_SECRET"`
OrganizationName *string `json:"ORGANIZATION_NAME"` OrganizationName *string `json:"ORGANIZATION_NAME"`
OrganizationLogo *string `json:"ORGANIZATION_LOGO"` OrganizationLogo *string `json:"ORGANIZATION_LOGO"`
} }

View File

@@ -20,6 +20,7 @@ type Meta {
is_github_login_enabled: Boolean! is_github_login_enabled: Boolean!
is_linkedin_login_enabled: Boolean! is_linkedin_login_enabled: Boolean!
is_apple_login_enabled: Boolean! is_apple_login_enabled: Boolean!
is_twitter_login_enabled: Boolean!
is_email_verification_enabled: Boolean! is_email_verification_enabled: Boolean!
is_basic_authentication_enabled: Boolean! is_basic_authentication_enabled: Boolean!
is_magic_link_login_enabled: Boolean! is_magic_link_login_enabled: Boolean!
@@ -141,6 +142,8 @@ type Env {
LINKEDIN_CLIENT_SECRET: String LINKEDIN_CLIENT_SECRET: String
APPLE_CLIENT_ID: String APPLE_CLIENT_ID: String
APPLE_CLIENT_SECRET: String APPLE_CLIENT_SECRET: String
TWITTER_CLIENT_ID: String
TWITTER_CLIENT_SECRET: String
ORGANIZATION_NAME: String ORGANIZATION_NAME: String
ORGANIZATION_LOGO: String ORGANIZATION_LOGO: String
} }
@@ -245,6 +248,8 @@ input UpdateEnvInput {
LINKEDIN_CLIENT_SECRET: String LINKEDIN_CLIENT_SECRET: String
APPLE_CLIENT_ID: String APPLE_CLIENT_ID: String
APPLE_CLIENT_SECRET: String APPLE_CLIENT_SECRET: String
TWITTER_CLIENT_ID: String
TWITTER_CLIENT_SECRET: String
ORGANIZATION_NAME: String ORGANIZATION_NAME: String
ORGANIZATION_LOGO: String ORGANIZATION_LOGO: String
} }

View File

@@ -248,7 +248,7 @@ func AuthorizeHandler() gin.HandlerFunc {
return return
} }
memorystore.Provider.SetUserSession(user.ID, constants.TokenTypeSessionToken+"_"+newSessionTokenData.Nonce, newSessionToken) memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+newSessionTokenData.Nonce, newSessionToken)
cookie.SetSession(gc, newSessionToken) cookie.SetSession(gc, newSessionToken)
code := uuid.New().String() code := uuid.New().String()
memorystore.Provider.SetState(codeChallenge, code+"@"+newSessionToken) memorystore.Provider.SetState(codeChallenge, code+"@"+newSessionToken)

View File

@@ -67,6 +67,8 @@ func OAuthCallbackHandler() gin.HandlerFunc {
user, err = processLinkedInUserInfo(code) user, err = processLinkedInUserInfo(code)
case constants.AuthRecipeMethodApple: case constants.AuthRecipeMethodApple:
user, err = processAppleUserInfo(code) user, err = processAppleUserInfo(code)
case constants.AuthRecipeMethodTwitter:
user, err = processTwitterUserInfo(code, sessionState)
default: default:
log.Info("Invalid oauth provider") log.Info("Invalid oauth provider")
err = fmt.Errorf(`invalid oauth provider`) err = fmt.Errorf(`invalid oauth provider`)
@@ -285,9 +287,9 @@ func processGithubUserInfo(code string) (models.User, error) {
log.Debug("Failed to create github user info request: ", err) log.Debug("Failed to create github user info request: ", err)
return user, fmt.Errorf("error creating github user info request: %s", err.Error()) return user, fmt.Errorf("error creating github user info request: %s", err.Error())
} }
req.Header = http.Header{ req.Header.Set(
"Authorization": []string{fmt.Sprintf("token %s", oauth2Token.AccessToken)}, "Authorization", fmt.Sprintf("token %s", oauth2Token.AccessToken),
} )
response, err := client.Do(req) response, err := client.Do(req)
if err != nil { if err != nil {
@@ -329,14 +331,14 @@ func processGithubUserInfo(code string) (models.User, error) {
} }
// fetch using /users/email endpoint // fetch using /users/email endpoint
req, err := http.NewRequest("GET", constants.GithubUserEmails, nil) req, err := http.NewRequest(http.MethodGet, constants.GithubUserEmails, nil)
if err != nil { if err != nil {
log.Debug("Failed to create github emails request: ", err) log.Debug("Failed to create github emails request: ", err)
return user, fmt.Errorf("error creating github user info request: %s", err.Error()) return user, fmt.Errorf("error creating github user info request: %s", err.Error())
} }
req.Header = http.Header{ req.Header.Set(
"Authorization": []string{fmt.Sprintf("token %s", oauth2Token.AccessToken)}, "Authorization", fmt.Sprintf("token %s", oauth2Token.AccessToken),
} )
response, err := client.Do(req) response, err := client.Do(req)
if err != nil { if err != nil {
@@ -564,3 +566,70 @@ func processAppleUserInfo(code string) (models.User, error) {
return user, err return user, err
} }
func processTwitterUserInfo(code, verifier string) (models.User, error) {
user := models.User{}
oauth2Token, err := oauth.OAuthProviders.TwitterConfig.Exchange(oauth2.NoContext, code, oauth2.SetAuthURLParam("code_verifier", verifier))
if err != nil {
log.Debug("Failed to exchange code for token: ", err)
return user, fmt.Errorf("invalid twitter exchange code: %s", err.Error())
}
client := http.Client{}
req, err := http.NewRequest("GET", constants.TwitterUserInfoURL, nil)
if err != nil {
log.Debug("Failed to create Twitter user info request: ", err)
return user, fmt.Errorf("error creating Twitter user info request: %s", err.Error())
}
req.Header = http.Header{
"Authorization": []string{fmt.Sprintf("Bearer %s", oauth2Token.AccessToken)},
}
response, err := client.Do(req)
if err != nil {
log.Debug("Failed to request Twitter user info: ", err)
return user, err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Debug("Failed to read Twitter user info response body: ", err)
return user, fmt.Errorf("failed to read Twitter response body: %s", err.Error())
}
if response.StatusCode >= 400 {
log.Debug("Failed to request Twitter user info: ", string(body))
return user, fmt.Errorf("failed to request Twitter user info: %s", string(body))
}
responseRawData := make(map[string]interface{})
json.Unmarshal(body, &responseRawData)
userRawData := responseRawData["data"].(map[string]interface{})
// log.Info(userRawData)
// Twitter API does not return E-Mail adresses by default. For that case special privileges have
// to be granted on a per-App basis. See https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/get-account-verify_credentials
// Currently Twitter API only provides the full name of a user. To fill givenName and familyName
// the full name will be split at the first whitespace. This approach will not be valid for all name combinations
nameArr := strings.SplitAfterN(userRawData["name"].(string), " ", 2)
firstName := nameArr[0]
lastName := ""
if len(nameArr) == 2 {
lastName = nameArr[1]
}
nickname := userRawData["username"].(string)
profilePicture := userRawData["profile_image_url"].(string)
user = models.User{
GivenName: &firstName,
FamilyName: &lastName,
Picture: &profilePicture,
Nickname: &nickname,
}
return user, nil
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/oauth" "github.com/authorizerdev/authorizer/server/oauth"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators" "github.com/authorizerdev/authorizer/server/validators"
) )
@@ -95,7 +96,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
} }
oauthStateString := state + "___" + redirectURI + "___" + roles + "___" + strings.Join(scope, ",") oauthStateString := state + "___" + redirectURI + "___" + roles + "___" + strings.Join(scope, " ")
provider := c.Param("oauth_provider") provider := c.Param("oauth_provider")
isProviderConfigured := true isProviderConfigured := true
@@ -169,6 +170,26 @@ func OAuthLoginHandler() gin.HandlerFunc {
oauth.OAuthProviders.LinkedInConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodLinkedIn oauth.OAuthProviders.LinkedInConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodLinkedIn
url := oauth.OAuthProviders.LinkedInConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.LinkedInConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
case constants.AuthRecipeMethodTwitter:
if oauth.OAuthProviders.TwitterConfig == nil {
log.Debug("Twitter OAuth provider is not configured")
isProviderConfigured = false
break
}
verifier, challenge := utils.GenerateCodeChallenge()
err := memorystore.Provider.SetState(oauthStateString, verifier)
if err != nil {
log.Debug("Error setting state: ", err)
c.JSON(500, gin.H{
"error": "internal server error",
})
return
}
oauth.OAuthProviders.TwitterConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodTwitter
url := oauth.OAuthProviders.TwitterConfig.AuthCodeURL(oauthStateString, oauth2.SetAuthURLParam("code_challenge", challenge), oauth2.SetAuthURLParam("code_challenge_method", "S256"))
c.Redirect(http.StatusTemporaryRedirect, url)
case constants.AuthRecipeMethodApple: case constants.AuthRecipeMethodApple:
if oauth.OAuthProviders.AppleConfig == nil { if oauth.OAuthProviders.AppleConfig == nil {
log.Debug("Apple OAuth provider is not configured") log.Debug("Apple OAuth provider is not configured")

View File

@@ -76,7 +76,6 @@ func TokenHandler() gin.HandlerFunc {
sessionKey := "" sessionKey := ""
if isAuthorizationCodeGrant { if isAuthorizationCodeGrant {
if codeVerifier == "" { if codeVerifier == "" {
log.Debug("Code verifier is empty") log.Debug("Code verifier is empty")
gc.JSON(http.StatusBadRequest, gin.H{ gc.JSON(http.StatusBadRequest, gin.H{
@@ -134,15 +133,18 @@ func TokenHandler() gin.HandlerFunc {
}) })
return return
} }
userID = claims.Subject userID = claims.Subject
roles = claims.Roles roles = claims.Roles
scope = claims.Scope scope = claims.Scope
loginMethod = claims.LoginMethod loginMethod = claims.LoginMethod
// rollover the session for security // rollover the session for security
sessionKey = userID sessionKey = userID
if loginMethod != "" { if loginMethod != "" {
sessionKey = loginMethod + ":" + userID sessionKey = loginMethod + ":" + userID
} }
go memorystore.Provider.DeleteUserSession(sessionKey, claims.Nonce) go memorystore.Provider.DeleteUserSession(sessionKey, claims.Nonce)
} else { } else {
// validate refresh token // validate refresh token

View File

@@ -109,6 +109,7 @@ func main() {
router := routes.InitRouter(log) router := routes.InitRouter(log)
log.Info("Starting Authorizer: ", VERSION) log.Info("Starting Authorizer: ", VERSION)
port, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyPort) port, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyPort)
log.Info("Authorizer running at PORT: ", port)
if err != nil { if err != nil {
log.Info("Error while getting port from env using default port 8080: ", err) log.Info("Error while getting port from env using default port 8080: ", err)
port = "8080" port = "8080"

View File

@@ -34,6 +34,8 @@ func InitMemStore() error {
constants.EnvKeyIsEmailServiceEnabled: false, constants.EnvKeyIsEmailServiceEnabled: false,
constants.EnvKeyEnforceMultiFactorAuthentication: false, constants.EnvKeyEnforceMultiFactorAuthentication: false,
constants.EnvKeyDisableMultiFactorAuthentication: false, constants.EnvKeyDisableMultiFactorAuthentication: false,
constants.EnvKeyAppCookieSecure: true,
constants.EnvKeyAdminCookieSecure: true,
} }
requiredEnvs := RequiredEnvStoreObj.GetRequiredEnv() requiredEnvs := RequiredEnvStoreObj.GetRequiredEnv()

View File

@@ -7,7 +7,7 @@ import (
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
) )
// SetUserSession sets the user session // SetUserSession sets the user session for given user identifier in form recipe:user_id
func (c *provider) SetUserSession(userId, key, token string) error { func (c *provider) SetUserSession(userId, key, token string) error {
c.sessionStore.Set(userId, key, token) c.sessionStore.Set(userId, key, token)
return nil return nil
@@ -34,6 +34,7 @@ func (c *provider) DeleteAllUserSessions(userId string) error {
constants.AuthRecipeMethodGithub, constants.AuthRecipeMethodGithub,
constants.AuthRecipeMethodGoogle, constants.AuthRecipeMethodGoogle,
constants.AuthRecipeMethodLinkedIn, constants.AuthRecipeMethodLinkedIn,
constants.AuthRecipeMethodTwitter,
} }
for _, namespace := range namespaces { for _, namespace := range namespaces {

View File

@@ -2,7 +2,7 @@ package providers
// Provider defines current memory store provider // Provider defines current memory store provider
type Provider interface { type Provider interface {
// SetUserSession sets the user session // SetUserSession sets the user session for given user identifier in form recipe:user_id
SetUserSession(userId, key, token string) error SetUserSession(userId, key, token string) error
// GetAllUserSessions returns all the user sessions from the session store // GetAllUserSessions returns all the user sessions from the session store
GetAllUserSessions(userId string) (map[string]string, error) GetAllUserSessions(userId string) (map[string]string, error)

View File

@@ -14,7 +14,7 @@ var (
envStorePrefix = "authorizer_env" envStorePrefix = "authorizer_env"
) )
// SetUserSession sets the user session in redis store. // SetUserSession sets the user session for given user identifier in form recipe:user_id
func (c *provider) SetUserSession(userId, key, token string) error { func (c *provider) SetUserSession(userId, key, token string) error {
err := c.store.HSet(c.ctx, userId, key, token).Err() err := c.store.HSet(c.ctx, userId, key, token).Err()
if err != nil { if err != nil {
@@ -71,6 +71,7 @@ func (c *provider) DeleteAllUserSessions(userID string) error {
constants.AuthRecipeMethodGithub, constants.AuthRecipeMethodGithub,
constants.AuthRecipeMethodGoogle, constants.AuthRecipeMethodGoogle,
constants.AuthRecipeMethodLinkedIn, constants.AuthRecipeMethodLinkedIn,
constants.AuthRecipeMethodTwitter,
} }
for _, namespace := range namespaces { for _, namespace := range namespaces {
err := c.store.Del(c.ctx, namespace+":"+userID).Err() err := c.store.Del(c.ctx, namespace+":"+userID).Err()
@@ -160,7 +161,7 @@ func (c *provider) GetEnvStore() (map[string]interface{}, error) {
return nil, err return nil, err
} }
for key, value := range data { for key, value := range data {
if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword || key == constants.EnvKeyIsEmailServiceEnabled || key == constants.EnvKeyEnforceMultiFactorAuthentication || key == constants.EnvKeyDisableMultiFactorAuthentication { if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword || key == constants.EnvKeyIsEmailServiceEnabled || key == constants.EnvKeyEnforceMultiFactorAuthentication || key == constants.EnvKeyDisableMultiFactorAuthentication || key == constants.EnvKeyAppCookieSecure || key == constants.EnvKeyAdminCookieSecure {
boolValue, err := strconv.ParseBool(value) boolValue, err := strconv.ParseBool(value)
if err != nil { if err != nil {
return res, err return res, err

View File

@@ -20,6 +20,7 @@ type OAuthProvider struct {
FacebookConfig *oauth2.Config FacebookConfig *oauth2.Config
LinkedInConfig *oauth2.Config LinkedInConfig *oauth2.Config
AppleConfig *oauth2.Config AppleConfig *oauth2.Config
TwitterConfig *oauth2.Config
} }
// OIDCProviders is a struct that contains reference all the OpenID providers // OIDCProviders is a struct that contains reference all the OpenID providers
@@ -74,6 +75,7 @@ func InitOAuth() error {
ClientSecret: githubClientSecret, ClientSecret: githubClientSecret,
RedirectURL: "/oauth_callback/github", RedirectURL: "/oauth_callback/github",
Endpoint: githubOAuth2.Endpoint, Endpoint: githubOAuth2.Endpoint,
Scopes: []string{"read:user", "user:email"},
} }
} }
@@ -133,5 +135,28 @@ func InitOAuth() error {
} }
} }
twitterClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwitterClientID)
if err != nil {
twitterClientID = ""
}
twitterClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwitterClientSecret)
if err != nil {
twitterClientSecret = ""
}
if twitterClientID != "" && twitterClientSecret != "" {
OAuthProviders.TwitterConfig = &oauth2.Config{
ClientID: twitterClientID,
ClientSecret: twitterClientSecret,
RedirectURL: "/oauth_callback/twitter",
Endpoint: oauth2.Endpoint{
// Endpoint is currently not yet part of oauth2-package. See https://go-review.googlesource.com/c/oauth2/+/350889 for status
AuthURL: "https://twitter.com/i/oauth2/authorize",
TokenURL: "https://api.twitter.com/2/oauth2/token",
AuthStyle: oauth2.AuthStyleInHeader,
},
Scopes: []string{"tweet.read", "users.read"},
}
}
return nil return nil
} }

View File

@@ -11,8 +11,8 @@ import (
) )
// GetHost returns hostname from request context // GetHost returns hostname from request context
// if X-Authorizer-URL header is set it is given highest priority // if EnvKeyAuthorizerURL is set it is given highest priority.
// if EnvKeyAuthorizerURL is set it is given second highest priority. // if X-Authorizer-URL header is set it is given second highest priority
// if above 2 are not set the requesting host name is used // if above 2 are not set the requesting host name is used
func GetHost(c *gin.Context) string { func GetHost(c *gin.Context) string {
authorizerURL, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) authorizerURL, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL)

View File

@@ -50,6 +50,34 @@ func DeleteUserResolver(ctx context.Context, params model.DeleteUserInput) (*mod
} }
go func() { go func() {
// delete otp for given email
otp, err := db.Provider.GetOTPByEmail(ctx, user.Email)
if err != nil {
log.Infof("No OTP found for email (%s): %v", user.Email, err)
// continue
} else {
err := db.Provider.DeleteOTP(ctx, otp)
if err != nil {
log.Debugf("Failed to delete otp for given email (%s): %v", user.Email, err)
// continue
}
}
// delete verification requests for given email
for _, vt := range constants.VerificationTypes {
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, user.Email, vt)
if err != nil {
log.Infof("No verification verification request found for email: %s, verification_request_type: %s. %v", user.Email, vt, err)
// continue
} else {
err := db.Provider.DeleteVerificationRequest(ctx, verificationRequest)
if err != nil {
log.Debugf("Failed to DeleteVerificationRequest for email: %s, verification_request_type: %s. %v", user.Email, vt, err)
// continue
}
}
}
memorystore.Provider.DeleteAllUserSessions(user.ID) memorystore.Provider.DeleteAllUserSessions(user.ID)
utils.RegisterEvent(ctx, constants.UserDeletedWebhookEvent, "", user) utils.RegisterEvent(ctx, constants.UserDeletedWebhookEvent, "", user)
}() }()

View File

@@ -143,6 +143,13 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
if val, ok := store[constants.EnvKeyAppleClientSecret]; ok { if val, ok := store[constants.EnvKeyAppleClientSecret]; ok {
res.AppleClientSecret = refs.NewStringRef(val.(string)) res.AppleClientSecret = refs.NewStringRef(val.(string))
} }
if val, ok := store[constants.EnvKeyTwitterClientID]; ok {
res.TwitterClientID = refs.NewStringRef(val.(string))
}
if val, ok := store[constants.EnvKeyTwitterClientSecret]; ok {
res.TwitterClientSecret = refs.NewStringRef(val.(string))
}
if val, ok := store[constants.EnvKeyOrganizationName]; ok { if val, ok := store[constants.EnvKeyOrganizationName]; ok {
res.OrganizationName = refs.NewStringRef(val.(string)) res.OrganizationName = refs.NewStringRef(val.(string))
} }

View File

@@ -50,7 +50,7 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
user, err := db.Provider.GetUserByEmail(ctx, params.Email) user, err := db.Provider.GetUserByEmail(ctx, params.Email)
if err != nil { if err != nil {
log.Debug("Failed to get user by email: ", err) log.Debug("Failed to get user by email: ", err)
return res, fmt.Errorf(`user with this email not found`) return res, fmt.Errorf(`bad user credentials`)
} }
if user.RevokedTimestamp != nil { if user.RevokedTimestamp != nil {
@@ -72,7 +72,7 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
if err != nil { if err != nil {
log.Debug("Failed to compare password: ", err) log.Debug("Failed to compare password: ", err)
return res, fmt.Errorf(`invalid password`) return res, fmt.Errorf(`bad user credentials`)
} }
defaultRolesString, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyDefaultRoles) defaultRolesString, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyDefaultRoles)

View File

@@ -77,6 +77,18 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) {
githubClientSecret = "" githubClientSecret = ""
} }
twitterClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwitterClientID)
if err != nil {
log.Debug("Failed to get Twitter Client ID from environment variable", err)
twitterClientID = ""
}
twitterClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwitterClientSecret)
if err != nil {
log.Debug("Failed to get Twitter Client Secret from environment variable", err)
twitterClientSecret = ""
}
isBasicAuthDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) isBasicAuthDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication)
if err != nil { if err != nil {
log.Debug("Failed to get Disable Basic Authentication from environment variable", err) log.Debug("Failed to get Disable Basic Authentication from environment variable", err)
@@ -121,6 +133,7 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) {
IsFacebookLoginEnabled: facebookClientID != "" && facebookClientSecret != "", IsFacebookLoginEnabled: facebookClientID != "" && facebookClientSecret != "",
IsLinkedinLoginEnabled: linkedClientID != "" && linkedInClientSecret != "", IsLinkedinLoginEnabled: linkedClientID != "" && linkedInClientSecret != "",
IsAppleLoginEnabled: appleClientID != "" && appleClientSecret != "", IsAppleLoginEnabled: appleClientID != "" && appleClientSecret != "",
IsTwitterLoginEnabled: twitterClientID != "" && twitterClientSecret != "",
IsBasicAuthenticationEnabled: !isBasicAuthDisabled, IsBasicAuthenticationEnabled: !isBasicAuthDisabled,
IsEmailVerificationEnabled: !isEmailVerificationDisabled, IsEmailVerificationEnabled: !isEmailVerificationDisabled,
IsMagicLinkLoginEnabled: !isMagicLinkLoginDisabled, IsMagicLinkLoginEnabled: !isMagicLinkLoginDisabled,

View File

@@ -31,6 +31,7 @@ func clearSessionIfRequired(currentData, updatedData map[string]interface{}) {
isCurrentGoogleLoginEnabled := currentData[constants.EnvKeyGoogleClientID] != nil && currentData[constants.EnvKeyGoogleClientSecret] != nil && currentData[constants.EnvKeyGoogleClientID].(string) != "" && currentData[constants.EnvKeyGoogleClientSecret].(string) != "" isCurrentGoogleLoginEnabled := currentData[constants.EnvKeyGoogleClientID] != nil && currentData[constants.EnvKeyGoogleClientSecret] != nil && currentData[constants.EnvKeyGoogleClientID].(string) != "" && currentData[constants.EnvKeyGoogleClientSecret].(string) != ""
isCurrentGithubLoginEnabled := currentData[constants.EnvKeyGithubClientID] != nil && currentData[constants.EnvKeyGithubClientSecret] != nil && currentData[constants.EnvKeyGithubClientID].(string) != "" && currentData[constants.EnvKeyGithubClientSecret].(string) != "" isCurrentGithubLoginEnabled := currentData[constants.EnvKeyGithubClientID] != nil && currentData[constants.EnvKeyGithubClientSecret] != nil && currentData[constants.EnvKeyGithubClientID].(string) != "" && currentData[constants.EnvKeyGithubClientSecret].(string) != ""
isCurrentLinkedInLoginEnabled := currentData[constants.EnvKeyLinkedInClientID] != nil && currentData[constants.EnvKeyLinkedInClientSecret] != nil && currentData[constants.EnvKeyLinkedInClientID].(string) != "" && currentData[constants.EnvKeyLinkedInClientSecret].(string) != "" isCurrentLinkedInLoginEnabled := currentData[constants.EnvKeyLinkedInClientID] != nil && currentData[constants.EnvKeyLinkedInClientSecret] != nil && currentData[constants.EnvKeyLinkedInClientID].(string) != "" && currentData[constants.EnvKeyLinkedInClientSecret].(string) != ""
isCurrentTwitterLoginEnabled := currentData[constants.EnvKeyTwitterClientID] != nil && currentData[constants.EnvKeyTwitterClientSecret] != nil && currentData[constants.EnvKeyTwitterClientID].(string) != "" && currentData[constants.EnvKeyTwitterClientSecret].(string) != ""
isUpdatedBasicAuthEnabled := !updatedData[constants.EnvKeyDisableBasicAuthentication].(bool) isUpdatedBasicAuthEnabled := !updatedData[constants.EnvKeyDisableBasicAuthentication].(bool)
isUpdatedMagicLinkLoginEnabled := !updatedData[constants.EnvKeyDisableMagicLinkLogin].(bool) isUpdatedMagicLinkLoginEnabled := !updatedData[constants.EnvKeyDisableMagicLinkLogin].(bool)
@@ -39,6 +40,7 @@ func clearSessionIfRequired(currentData, updatedData map[string]interface{}) {
isUpdatedGoogleLoginEnabled := updatedData[constants.EnvKeyGoogleClientID] != nil && updatedData[constants.EnvKeyGoogleClientSecret] != nil && updatedData[constants.EnvKeyGoogleClientID].(string) != "" && updatedData[constants.EnvKeyGoogleClientSecret].(string) != "" isUpdatedGoogleLoginEnabled := updatedData[constants.EnvKeyGoogleClientID] != nil && updatedData[constants.EnvKeyGoogleClientSecret] != nil && updatedData[constants.EnvKeyGoogleClientID].(string) != "" && updatedData[constants.EnvKeyGoogleClientSecret].(string) != ""
isUpdatedGithubLoginEnabled := updatedData[constants.EnvKeyGithubClientID] != nil && updatedData[constants.EnvKeyGithubClientSecret] != nil && updatedData[constants.EnvKeyGithubClientID].(string) != "" && updatedData[constants.EnvKeyGithubClientSecret].(string) != "" isUpdatedGithubLoginEnabled := updatedData[constants.EnvKeyGithubClientID] != nil && updatedData[constants.EnvKeyGithubClientSecret] != nil && updatedData[constants.EnvKeyGithubClientID].(string) != "" && updatedData[constants.EnvKeyGithubClientSecret].(string) != ""
isUpdatedLinkedInLoginEnabled := updatedData[constants.EnvKeyLinkedInClientID] != nil && updatedData[constants.EnvKeyLinkedInClientSecret] != nil && updatedData[constants.EnvKeyLinkedInClientID].(string) != "" && updatedData[constants.EnvKeyLinkedInClientSecret].(string) != "" isUpdatedLinkedInLoginEnabled := updatedData[constants.EnvKeyLinkedInClientID] != nil && updatedData[constants.EnvKeyLinkedInClientSecret] != nil && updatedData[constants.EnvKeyLinkedInClientID].(string) != "" && updatedData[constants.EnvKeyLinkedInClientSecret].(string) != ""
isUpdatedTwitterLoginEnabled := updatedData[constants.EnvKeyTwitterClientID] != nil && updatedData[constants.EnvKeyTwitterClientSecret] != nil && updatedData[constants.EnvKeyTwitterClientID].(string) != "" && updatedData[constants.EnvKeyTwitterClientSecret].(string) != ""
if isCurrentBasicAuthEnabled && !isUpdatedBasicAuthEnabled { if isCurrentBasicAuthEnabled && !isUpdatedBasicAuthEnabled {
memorystore.Provider.DeleteSessionForNamespace(constants.AuthRecipeMethodBasicAuth) memorystore.Provider.DeleteSessionForNamespace(constants.AuthRecipeMethodBasicAuth)
@@ -67,6 +69,10 @@ func clearSessionIfRequired(currentData, updatedData map[string]interface{}) {
if isCurrentLinkedInLoginEnabled && !isUpdatedLinkedInLoginEnabled { if isCurrentLinkedInLoginEnabled && !isUpdatedLinkedInLoginEnabled {
memorystore.Provider.DeleteSessionForNamespace(constants.AuthRecipeMethodLinkedIn) memorystore.Provider.DeleteSessionForNamespace(constants.AuthRecipeMethodLinkedIn)
} }
if isCurrentTwitterLoginEnabled && !isUpdatedTwitterLoginEnabled {
memorystore.Provider.DeleteSessionForNamespace(constants.AuthRecipeMethodTwitter)
}
} }
// UpdateEnvResolver is a resolver for update config mutation // UpdateEnvResolver is a resolver for update config mutation

View File

@@ -298,7 +298,6 @@ func ValidateBrowserSession(gc *gin.Context, encryptedSession string) (*SessionD
if res.LoginMethod != "" { if res.LoginMethod != "" {
sessionStoreKey = res.LoginMethod + ":" + res.Subject sessionStoreKey = res.LoginMethod + ":" + res.Subject
} }
token, err := memorystore.Provider.GetUserSession(sessionStoreKey, constants.TokenTypeSessionToken+"_"+res.Nonce) token, err := memorystore.Provider.GetUserSession(sessionStoreKey, constants.TokenTypeSessionToken+"_"+res.Nonce)
if token == "" || err != nil { if token == "" || err != nil {
log.Debug("invalid browser session:", err) log.Debug("invalid browser session:", err)

32
server/utils/pkce.go Normal file
View File

@@ -0,0 +1,32 @@
package utils
import (
"crypto/sha256"
b64 "encoding/base64"
"math/rand"
"strings"
"time"
)
const (
length = 32
)
// GenerateCodeChallenge creates PKCE-Code-Challenge
// and returns the verifier and challenge
func GenerateCodeChallenge() (string, string) {
// Generate Verifier
randGenerator := rand.New(rand.NewSource(time.Now().UnixNano()))
randomBytes := make([]byte, length)
for i := 0; i < length; i++ {
randomBytes[i] = byte(randGenerator.Intn(255))
}
verifier := strings.Trim(b64.URLEncoding.EncodeToString(randomBytes), "=")
// Generate Challenge
rawChallenge := sha256.New()
rawChallenge.Write([]byte(verifier))
challenge := strings.Trim(b64.URLEncoding.EncodeToString(rawChallenge.Sum(nil)), "=")
return verifier, challenge
}

View File

@@ -30,8 +30,8 @@ func IsValidOrigin(url string) bool {
replacedString := origin replacedString := origin
// if has regex whitelisted domains // if has regex whitelisted domains
if strings.Contains(origin, "*") { if strings.Contains(origin, "*") {
replacedString = strings.Replace(origin, ".", "\\.", -1) replacedString = strings.ReplaceAll(origin, ".", "\\.")
replacedString = strings.Replace(replacedString, "*", ".*", -1) replacedString = strings.ReplaceAll(replacedString, "*", ".*")
if strings.HasPrefix(replacedString, ".*") { if strings.HasPrefix(replacedString, ".*") {
replacedString += "\\b" replacedString += "\\b"