Compare commits

..

26 Commits
1.0.1 ... 1.1.3

Author SHA1 Message Date
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
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
35 changed files with 903 additions and 442 deletions

View File

@@ -7,7 +7,7 @@
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 ## Table of contents
@@ -19,7 +19,7 @@
# 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
@@ -65,11 +65,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

30
app/package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^1.0.0", "@authorizerdev/authorizer-react": "^1.1.0",
"@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,9 +26,9 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-js": { "node_modules/@authorizerdev/authorizer-js": {
"version": "1.0.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.1.0.tgz",
"integrity": "sha512-TtXA8y06CIZ5f+nk1tgPiSpGR9neCkGHtmKLqGWjWPdObDfekRU5qMtpC2S2uEljAI53mnueLZKmbd9hrReTag==", "integrity": "sha512-MdEw1SjhIm7pXq20AscHSbnAta2PC3w7GNBY52/OzmlBXUGH3ooUQX/aszbYOse3FlhapcrGrRvg4sNM7faGAg==",
"dependencies": { "dependencies": {
"cross-fetch": "^3.1.5" "cross-fetch": "^3.1.5"
}, },
@@ -37,11 +37,11 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-react": { "node_modules/@authorizerdev/authorizer-react": {
"version": "1.0.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.0.tgz",
"integrity": "sha512-lXckUe46LTcH+hFxIxPEewQR1/ktd2awoqZsMZZaa0AjQJoekJaUcouBuX0F66VyQG0qezuyEQrye0Z93Ffgug==", "integrity": "sha512-8ooyBREFI6ohHApVOPQitFr7T0w0SlpEVZruvU9oqa8OQ77UBxLQh+PCRKKPw7FeQRdCdh/VQyl17W7Xphp1NA==",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": "^1.0.0", "@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"
@@ -860,19 +860,19 @@
}, },
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": { "@authorizerdev/authorizer-js": {
"version": "1.0.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.1.0.tgz",
"integrity": "sha512-TtXA8y06CIZ5f+nk1tgPiSpGR9neCkGHtmKLqGWjWPdObDfekRU5qMtpC2S2uEljAI53mnueLZKmbd9hrReTag==", "integrity": "sha512-MdEw1SjhIm7pXq20AscHSbnAta2PC3w7GNBY52/OzmlBXUGH3ooUQX/aszbYOse3FlhapcrGrRvg4sNM7faGAg==",
"requires": { "requires": {
"cross-fetch": "^3.1.5" "cross-fetch": "^3.1.5"
} }
}, },
"@authorizerdev/authorizer-react": { "@authorizerdev/authorizer-react": {
"version": "1.0.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.0.tgz",
"integrity": "sha512-lXckUe46LTcH+hFxIxPEewQR1/ktd2awoqZsMZZaa0AjQJoekJaUcouBuX0F66VyQG0qezuyEQrye0Z93Ffgug==", "integrity": "sha512-8ooyBREFI6ohHApVOPQitFr7T0w0SlpEVZruvU9oqa8OQ77UBxLQh+PCRKKPw7FeQRdCdh/VQyl17W7Xphp1NA==",
"requires": { "requires": {
"@authorizerdev/authorizer-js": "^1.0.0", "@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"

View File

@@ -11,7 +11,7 @@
"author": "Lakhan Samani", "author": "Lakhan Samani",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^1.0.0", "@authorizerdev/authorizer-react": "^1.1.0",
"@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

@@ -85,6 +85,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

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

16
server/env/env.go vendored
View File

@@ -72,6 +72,8 @@ 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)
@@ -380,6 +382,20 @@ func InitAllEnv() error {
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, "/")
} }

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`)
@@ -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

@@ -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()

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
@@ -133,5 +134,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

@@ -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"