Compare commits

...

26 Commits

Author SHA1 Message Date
Lakhan Samani
74a8024131 feat: add integration test for invite_member 2022-03-15 12:09:54 +05:30
Lakhan Samani
5e6ee8d9b0 fix: setup-password flow 2022-03-15 09:57:09 +05:30
Lakhan Samani
3e7150f872 fix: redirect uri 2022-03-15 09:56:50 +05:30
Lakhan Samani
9a19552f72 feat: add resolver for inviting members 2022-03-15 08:53:48 +05:30
Lakhan Samani
1b387f7564 fix: getting version in meta api 2022-03-09 18:55:18 +05:30
Lakhan Samani
8e79ab77b2 Merge pull request #131 from authorizerdev/feat/open-id
Add open id authorization flow with PKCE
2022-03-09 17:27:16 +05:30
Lakhan Samani
2bf6b8f91d fix: remove log 2022-03-09 17:24:53 +05:30
Lakhan Samani
776c0fba8b chore: app dependencies 2022-03-09 17:21:55 +05:30
Lakhan Samani
dd64aa2e79 feat: add version info 2022-03-09 11:53:34 +05:30
Lakhan Samani
157b13baa7 fix: basic auth redirect 2022-03-09 10:10:39 +05:30
Lakhan Samani
d1e284116d fix: verification request model 2022-03-09 07:10:07 +05:30
Lakhan Samani
2f9725d8e1 fix: verification request 2022-03-09 06:41:38 +05:30
Lakhan Samani
ee7aea7bee fix: verify email 2022-03-08 22:55:45 +05:30
Lakhan Samani
5d73df0040 fix: magic link login 2022-03-08 22:41:33 +05:30
Lakhan Samani
60cd317e67 fix: add redirect url to logout 2022-03-08 21:32:42 +05:30
Lakhan Samani
f5bdc8db39 fix: refresh token store info 2022-03-08 21:13:23 +05:30
Lakhan Samani
9eca697a91 fix: refresh token param in string 2022-03-08 19:31:19 +05:30
Lakhan Samani
7136ee924d fix: rotate refresh token 2022-03-08 19:18:33 +05:30
Lakhan Samani
fd9eb7c733 fix: oauth state split 2022-03-08 19:13:45 +05:30
Lakhan Samani
917eaeb2ed feat: don't set cookie in case of offline_access 2022-03-08 18:51:46 +05:30
Lakhan Samani
3bb90acc9e feat: add revoke mutation + handler 2022-03-08 18:49:42 +05:30
Lakhan Samani
a69b8e290c feat: add ability to get access token based on refresh token 2022-03-08 14:56:46 +05:30
Lakhan Samani
674eeeea4e chore: bump authorizer-react 2022-03-08 14:20:11 +05:30
Lakhan Samani
8c2bf6ee0d fix: add token information in redirect url 2022-03-08 12:36:26 +05:30
Lakhan Samani
57bc091499 fix state management 2022-03-07 23:44:19 +05:30
Lakhan Samani
128a2a8f75 feat: add support for response mode 2022-03-07 18:49:18 +05:30
51 changed files with 1554 additions and 416 deletions

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": "0.7.0", "@authorizerdev/authorizer-react": "latest",
"@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",
@@ -24,9 +24,9 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-js": { "node_modules/@authorizerdev/authorizer-js": {
"version": "0.3.0", "version": "0.4.0-beta.3",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.3.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz",
"integrity": "sha512-KCE5Dw5MUnEgstBUayBriDQAOjqbxU7ixC00rTHAE6aD6TxJkeSls0vCTXpvt4iiKhFK6q9BhHwa/5NwWYpDBQ==", "integrity": "sha512-OGZc6I6cnpi/WkSotkjVIc3LEzl8pFeiohr8+Db9xWd75/oTfOZqWRuIHTnTc1FC+6Sv2EjTJ9Aa6lrloWG+NQ==",
"dependencies": { "dependencies": {
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1"
}, },
@@ -35,11 +35,11 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-react": { "node_modules/@authorizerdev/authorizer-react": {
"version": "0.7.0", "version": "0.9.0-beta.7",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.7.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz",
"integrity": "sha512-cAxUhodftIveSQt+rFuEA0CxjmbpVfE43ioZBwBxqWEuJHPdPH7bohOQRgTyA2xb3QVnh7kr607Tau13DO7qUA==", "integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": "^0.3.0", "@authorizerdev/authorizer-js": "^0.4.0-beta.3",
"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"
@@ -829,19 +829,19 @@
}, },
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": { "@authorizerdev/authorizer-js": {
"version": "0.3.0", "version": "0.4.0-beta.3",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.3.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz",
"integrity": "sha512-KCE5Dw5MUnEgstBUayBriDQAOjqbxU7ixC00rTHAE6aD6TxJkeSls0vCTXpvt4iiKhFK6q9BhHwa/5NwWYpDBQ==", "integrity": "sha512-OGZc6I6cnpi/WkSotkjVIc3LEzl8pFeiohr8+Db9xWd75/oTfOZqWRuIHTnTc1FC+6Sv2EjTJ9Aa6lrloWG+NQ==",
"requires": { "requires": {
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1"
} }
}, },
"@authorizerdev/authorizer-react": { "@authorizerdev/authorizer-react": {
"version": "0.7.0", "version": "0.9.0-beta.7",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.7.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz",
"integrity": "sha512-cAxUhodftIveSQt+rFuEA0CxjmbpVfE43ioZBwBxqWEuJHPdPH7bohOQRgTyA2xb3QVnh7kr607Tau13DO7qUA==", "integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==",
"requires": { "requires": {
"@authorizerdev/authorizer-js": "^0.3.0", "@authorizerdev/authorizer-js": "^0.4.0-beta.3",
"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

@@ -2,10 +2,33 @@ import React from 'react';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { AuthorizerProvider } from '@authorizerdev/authorizer-react'; import { AuthorizerProvider } from '@authorizerdev/authorizer-react';
import Root from './Root'; import Root from './Root';
import { createRandomString } from './utils/common';
export default function App() { export default function App() {
const searchParams = new URLSearchParams(window.location.search);
const state = searchParams.get('state') || createRandomString();
const scope = searchParams.get('scope')
? searchParams.get('scope')?.toString().split(' ')
: `openid profile email`;
const urlProps: Record<string, any> = {
state,
scope,
};
const redirectURL =
searchParams.get('redirect_uri') || searchParams.get('redirectURL');
if (redirectURL) {
urlProps.redirectURL = redirectURL;
} else {
urlProps.redirectURL = window.location.origin + '/app';
}
const globalState: Record<string, string> = {
// @ts-ignore // @ts-ignore
const globalState: Record<string, string> = window['__authorizer__']; ...window['__authorizer__'],
...urlProps,
};
return ( return (
<div <div
style={{ style={{
@@ -30,15 +53,7 @@ export default function App() {
/> />
<h1>{globalState.organizationName}</h1> <h1>{globalState.organizationName}</h1>
</div> </div>
<div <div className="container">
style={{
width: 400,
margin: `10px auto`,
border: `1px solid #D1D5DB`,
padding: `25px 20px`,
borderRadius: 5,
}}
>
<BrowserRouter> <BrowserRouter>
<AuthorizerProvider <AuthorizerProvider
config={{ config={{
@@ -46,7 +61,7 @@ export default function App() {
redirectURL: globalState.redirectURL, redirectURL: globalState.redirectURL,
}} }}
> >
<Root /> <Root globalState={globalState} />
</AuthorizerProvider> </AuthorizerProvider>
</BrowserRouter> </BrowserRouter>
</div> </div>

View File

@@ -1,19 +1,36 @@
import React, { useEffect, lazy, Suspense } from 'react'; import React, { useEffect, lazy, Suspense } from 'react';
import { Switch, Route } from 'react-router-dom'; import { Switch, Route } from 'react-router-dom';
import { useAuthorizer } from '@authorizerdev/authorizer-react'; import { useAuthorizer } from '@authorizerdev/authorizer-react';
import SetupPassword from './pages/setup-password';
const ResetPassword = lazy(() => import('./pages/rest-password')); const ResetPassword = lazy(() => import('./pages/rest-password'));
const Login = lazy(() => import('./pages/login')); const Login = lazy(() => import('./pages/login'));
const Dashboard = lazy(() => import('./pages/dashboard')); const Dashboard = lazy(() => import('./pages/dashboard'));
export default function Root() { export default function Root({
globalState,
}: {
globalState: Record<string, string>;
}) {
const { token, loading, config } = useAuthorizer(); const { token, loading, config } = useAuthorizer();
useEffect(() => { useEffect(() => {
if (token) { if (token) {
const url = new URL(config.redirectURL || '/app'); let redirectURL = config.redirectURL || '/app';
let params = `access_token=${token.access_token}&id_token=${token.id_token}&expires_in=${token.expires_in}&state=${globalState.state}`;
if (token.refresh_token) {
params += `&refresh_token=${token.refresh_token}`;
}
const url = new URL(redirectURL);
if (redirectURL.includes('?')) {
redirectURL = `${redirectURL}&${params}`;
} else {
redirectURL = `${redirectURL}?${params}`;
}
if (url.origin !== window.location.origin) { if (url.origin !== window.location.origin) {
window.location.href = config.redirectURL || '/app'; sessionStorage.removeItem('authorizer_state');
window.location.replace(redirectURL);
} }
} }
return () => {}; return () => {};
@@ -44,6 +61,9 @@ export default function Root() {
<Route path="/app/reset-password"> <Route path="/app/reset-password">
<ResetPassword /> <ResetPassword />
</Route> </Route>
<Route path="/app/setup-password">
<SetupPassword />
</Route>
</Switch> </Switch>
</Suspense> </Suspense>
); );

View File

@@ -1,5 +1,5 @@
body { body {
margin: 0; margin: 10;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif; sans-serif;
@@ -14,3 +14,17 @@ body {
*:after { *:after {
box-sizing: inherit; box-sizing: inherit;
} }
.container {
box-sizing: content-box;
border: 1px solid #d1d5db;
padding: 25px 20px;
border-radius: 5px;
}
@media only screen and (min-width: 768px) {
.container {
width: 400px;
margin: 0 auto;
}
}

View File

@@ -0,0 +1,12 @@
import React, { Fragment } from 'react';
import { AuthorizerResetPassword } from '@authorizerdev/authorizer-react';
export default function SetupPassword() {
return (
<Fragment>
<h1 style={{ textAlign: 'center' }}>Setup new Password</h1>
<br />
<AuthorizerResetPassword />
</Fragment>
);
}

22
app/src/utils/common.ts Normal file
View File

@@ -0,0 +1,22 @@
export const getCrypto = () => {
//ie 11.x uses msCrypto
return (window.crypto || (window as any).msCrypto) as Crypto;
};
export const createRandomString = () => {
const charset =
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_~.';
let random = '';
const randomValues = Array.from(
getCrypto().getRandomValues(new Uint8Array(43))
);
randomValues.forEach((v) => (random += charset[v % charset.length]));
return random;
};
export const createQueryParams = (params: any) => {
return Object.keys(params)
.filter((k) => typeof params[k] !== 'undefined')
.map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&');
};

View File

@@ -29,10 +29,11 @@ import {
} from 'react-icons/fi'; } from 'react-icons/fi';
import { IconType } from 'react-icons'; import { IconType } from 'react-icons';
import { ReactText } from 'react'; import { ReactText } from 'react';
import { useMutation } from 'urql'; import { useMutation, useQuery } from 'urql';
import { NavLink, useNavigate, useLocation } from 'react-router-dom'; import { NavLink, useNavigate, useLocation } from 'react-router-dom';
import { useAuthContext } from '../contexts/AuthContext'; import { useAuthContext } from '../contexts/AuthContext';
import { AdminLogout } from '../graphql/mutation'; import { AdminLogout } from '../graphql/mutation';
import { MetaQuery } from '../graphql/queries';
interface LinkItemProps { interface LinkItemProps {
name: string; name: string;
@@ -51,6 +52,7 @@ interface SidebarProps extends BoxProps {
export const Sidebar = ({ onClose, ...rest }: SidebarProps) => { export const Sidebar = ({ onClose, ...rest }: SidebarProps) => {
const { pathname } = useLocation(); const { pathname } = useLocation();
const [{ fetching, data }] = useQuery({ query: MetaQuery });
return ( return (
<Box <Box
transition="3s ease" transition="3s ease"
@@ -98,6 +100,19 @@ export const Sidebar = ({ onClose, ...rest }: SidebarProps) => {
> >
<NavItem icon={FiCode}>API Playground</NavItem> <NavItem icon={FiCode}>API Playground</NavItem>
</Link> </Link>
{data?.meta?.version && (
<Text
color="gray.600"
fontSize="sm"
textAlign="center"
position="absolute"
bottom="5"
left="7"
>
Current Version: {data.meta.version}
</Text>
)}
</Box> </Box>
); );
}; };

View File

@@ -1,3 +1,12 @@
export const MetaQuery = `
query MetaQuery {
meta {
version
client_id
}
}
`;
export const AdminSessionQuery = ` export const AdminSessionQuery = `
query { query {
_admin_session{ _admin_session{

View File

@@ -1,8 +1,10 @@
import { Box, Center, Flex, Image, Text } from '@chakra-ui/react'; import { Box, Flex, Image, Text, Spinner } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { LOGO_URL } from '../constants'; import { useQuery } from 'urql';
import { MetaQuery } from '../graphql/queries';
export function AuthLayout({ children }: { children: React.ReactNode }) { export function AuthLayout({ children }: { children: React.ReactNode }) {
const [{ fetching, data }] = useQuery({ query: MetaQuery });
return ( return (
<Flex <Flex
flexWrap="wrap" flexWrap="wrap"
@@ -23,9 +25,18 @@ export function AuthLayout({ children }: { children: React.ReactNode }) {
</Text> </Text>
</Flex> </Flex>
{fetching ? (
<Spinner />
) : (
<>
<Box p="6" m="5" rounded="5" bg="white" w="500px" shadow="xl"> <Box p="6" m="5" rounded="5" bg="white" w="500px" shadow="xl">
{children} {children}
</Box> </Box>
<Text color="gray.600" fontSize="sm">
Current Version: {data.meta.version}
</Text>
</>
)}
</Flex> </Flex>
); );
} }

View File

@@ -6,7 +6,6 @@ import {
useToast, useToast,
VStack, VStack,
Text, Text,
Divider,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useMutation } from 'urql'; import { useMutation } from 'urql';

View File

@@ -1,5 +1,7 @@
package constants package constants
var VERSION = "0.0.1"
const ( const (
// Envstore identifier // Envstore identifier
// StringStore string store identifier // StringStore string store identifier
@@ -13,8 +15,6 @@ const (
EnvKeyEnv = "ENV" EnvKeyEnv = "ENV"
// EnvKeyEnvPath key for cli arg variable ENV_PATH // EnvKeyEnvPath key for cli arg variable ENV_PATH
EnvKeyEnvPath = "ENV_PATH" EnvKeyEnvPath = "ENV_PATH"
// EnvKeyVersion key for build arg version
EnvKeyVersion = "VERSION"
// EnvKeyAuthorizerURL key for env variable AUTHORIZER_URL // EnvKeyAuthorizerURL key for env variable AUTHORIZER_URL
// TODO: remove support AUTHORIZER_URL env // TODO: remove support AUTHORIZER_URL env
EnvKeyAuthorizerURL = "AUTHORIZER_URL" EnvKeyAuthorizerURL = "AUTHORIZER_URL"

View File

@@ -94,6 +94,7 @@ func EncryptEnvData(data envstore.Store) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
encryptedConfig, err := EncryptAESEnv(configData) encryptedConfig, err := EncryptAESEnv(configData)
if err != nil { if err != nil {
return "", err return "", err

View File

@@ -12,7 +12,8 @@ type VerificationRequest struct {
CreatedAt int64 `json:"created_at" bson:"created_at"` CreatedAt int64 `json:"created_at" bson:"created_at"`
UpdatedAt int64 `json:"updated_at" bson:"updated_at"` UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"` Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"`
Nonce string `gorm:"type:char(36)" json:"nonce" bson:"nonce"` Nonce string `gorm:"type:text" json:"nonce" bson:"nonce"`
RedirectURI string `gorm:"type:text" json:"redirect_uri" bson:"redirect_uri"`
} }
func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest { func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest {
@@ -24,5 +25,7 @@ func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequ
CreatedAt: &v.CreatedAt, CreatedAt: &v.CreatedAt,
UpdatedAt: &v.UpdatedAt, UpdatedAt: &v.UpdatedAt,
Email: &v.Email, Email: &v.Email,
Nonce: &v.Nonce,
RedirectURI: &v.RedirectURI,
} }
} }

View File

@@ -21,7 +21,7 @@ func (p *provider) AddVerificationRequest(verificationRequest models.Verificatio
verificationRequest.UpdatedAt = time.Now().Unix() verificationRequest.UpdatedAt = time.Now().Unix()
result := p.db.Clauses(clause.OnConflict{ result := p.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}, {Name: "identifier"}}, Columns: []clause.Column{{Name: "email"}, {Name: "identifier"}},
DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at"}), DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at", "nonce", "redirect_uri"}),
}).Create(&verificationRequest) }).Create(&verificationRequest)
if result.Error != nil { if result.Error != nil {

View File

@@ -0,0 +1,113 @@
package email
import (
"log"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore"
)
// InviteEmail to send invite email
func InviteEmail(toEmail, token, url string) error {
// The receiver needs to be in slice as the receive supports multiple receiver
Receiver := []string{toEmail}
Subject := "Please accept the invitation"
message := `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="telephone=no" name="format-detection">
<title></title>
<!--[if (mso 16)]>
<style type="text/css">
a {}
</style>
<![endif]-->
<!--[if gte mso 9]><style>sup { font-size: 100%% !important; }</style><![endif]-->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG></o:AllowPNG>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body style="font-family: sans-serif;">
<div class="es-wrapper-color">
<!--[if gte mso 9]>
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
<v:fill type="tile" color="#ffffff"></v:fill>
</v:background>
<![endif]-->
<table class="es-wrapper" width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-email-paddings" valign="top">
<table class="es-content esd-footer-popover" cellspacing="0" cellpadding="0" align="center">
<tbody>
<tr>
<td class="esd-stripe" align="center">
<table class="es-content-body" style="border-left:1px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent;border-bottom:1px solid transparent;padding:20px 0px;" width="600" cellspacing="0" cellpadding="0" bgcolor="#ffffff" align="center">
<tbody>
<tr>
<td class="esd-structure es-p20t es-p40b es-p40r es-p40l" esd-custom-block-id="8537" align="left">
<table width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-container-frame" width="518" align="left">
<table width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-block-image es-m-txt-c es-p5b" style="font-size:0;padding:10px" align="center"><a target="_blank" clicktracking="off"><img src="{{.org_logo}}" alt="icon" style="display: block;" title="icon" width="30"></a></td>
</tr>
<tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;">
<td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;">
<p>Hi there 👋</p>
<p>Join us! You are invited to sign-up for <b>{{.org_name}}</b>. Please accept the invitation by clicking the clicking the button below.</p> <br/>
<a
clicktracking="off" href="{{.verification_url}}" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Get Started</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<div style="position: absolute; left: -9999px; top: -9999px; margin: 0px;"></div>
</body>
</html>
`
data := make(map[string]interface{}, 3)
data["org_logo"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo)
data["org_name"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName)
data["verification_url"] = url + "?token=" + token
message = addEmailTemplate(message, data, "invite_email.tmpl")
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
err := SendMail(Receiver, Subject, message)
if err != nil {
log.Println("=> error sending email:", err)
}
return err
}

View File

@@ -1,6 +1,8 @@
package email package email
import ( import (
"log"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
) )
@@ -103,5 +105,9 @@ func SendVerificationMail(toEmail, token, hostname string) error {
message = addEmailTemplate(message, data, "verify_email.tmpl") message = addEmailTemplate(message, data, "verify_email.tmpl")
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) // bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
return SendMail(Receiver, Subject, message) err := SendMail(Receiver, Subject, message)
if err != nil {
log.Println("=> error sending email:", err)
}
return err
} }

View File

@@ -112,7 +112,7 @@ func PersistEnv() error {
for key, value := range storeData.StringEnv { for key, value := range storeData.StringEnv {
// don't override unexposed envs // don't override unexposed envs
if key != constants.EnvKeyEncryptionKey && key != constants.EnvKeyClientID && key != constants.EnvKeyClientSecret && key != constants.EnvKeyJWK { if key != constants.EnvKeyEncryptionKey {
// check only for derivative keys // check only for derivative keys
// No need to check for ENCRYPTION_KEY which special key we use for encrypting config data // No need to check for ENCRYPTION_KEY which special key we use for encrypting config data
// as we have removed it from json // as we have removed it from json

View File

@@ -114,11 +114,13 @@ type ComplexityRoot struct {
AdminSignup func(childComplexity int, params model.AdminSignupInput) int AdminSignup func(childComplexity int, params model.AdminSignupInput) int
DeleteUser func(childComplexity int, params model.DeleteUserInput) int DeleteUser func(childComplexity int, params model.DeleteUserInput) int
ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int
InviteMembers func(childComplexity int, params model.InviteMemberInput) int
Login func(childComplexity int, params model.LoginInput) int Login func(childComplexity int, params model.LoginInput) int
Logout func(childComplexity int) int Logout func(childComplexity int) int
MagicLinkLogin func(childComplexity int, params model.MagicLinkLoginInput) int MagicLinkLogin func(childComplexity int, params model.MagicLinkLoginInput) int
ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int
ResetPassword func(childComplexity int, params model.ResetPasswordInput) int ResetPassword func(childComplexity int, params model.ResetPasswordInput) int
Revoke func(childComplexity int, params model.OAuthRevokeInput) int
Signup func(childComplexity int, params model.SignUpInput) int Signup func(childComplexity int, params model.SignUpInput) int
UpdateEnv func(childComplexity int, params model.UpdateEnvInput) int UpdateEnv func(childComplexity int, params model.UpdateEnvInput) int
UpdateProfile func(childComplexity int, params model.UpdateProfileInput) int UpdateProfile func(childComplexity int, params model.UpdateProfileInput) int
@@ -178,6 +180,8 @@ type ComplexityRoot struct {
Expires func(childComplexity int) int Expires func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
Identifier func(childComplexity int) int Identifier func(childComplexity int) int
Nonce func(childComplexity int) int
RedirectURI func(childComplexity int) int
Token func(childComplexity int) int Token func(childComplexity int) int
UpdatedAt func(childComplexity int) int UpdatedAt func(childComplexity int) int
} }
@@ -198,12 +202,14 @@ type MutationResolver interface {
ResendVerifyEmail(ctx context.Context, params model.ResendVerifyEmailInput) (*model.Response, error) ResendVerifyEmail(ctx context.Context, params model.ResendVerifyEmailInput) (*model.Response, error)
ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*model.Response, error) ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*model.Response, error)
ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error) ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error)
Revoke(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error)
DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error)
UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error) UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error)
AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error) AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error)
AdminLogin(ctx context.Context, params model.AdminLoginInput) (*model.Response, error) AdminLogin(ctx context.Context, params model.AdminLoginInput) (*model.Response, error)
AdminLogout(ctx context.Context) (*model.Response, error) AdminLogout(ctx context.Context) (*model.Response, error)
UpdateEnv(ctx context.Context, params model.UpdateEnvInput) (*model.Response, error) UpdateEnv(ctx context.Context, params model.UpdateEnvInput) (*model.Response, error)
InviteMembers(ctx context.Context, params model.InviteMemberInput) (*model.Response, error)
} }
type QueryResolver interface { type QueryResolver interface {
Meta(ctx context.Context) (*model.Meta, error) Meta(ctx context.Context) (*model.Meta, error)
@@ -656,6 +662,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.ForgotPassword(childComplexity, args["params"].(model.ForgotPasswordInput)), true return e.complexity.Mutation.ForgotPassword(childComplexity, args["params"].(model.ForgotPasswordInput)), true
case "Mutation._invite_members":
if e.complexity.Mutation.InviteMembers == nil {
break
}
args, err := ec.field_Mutation__invite_members_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.InviteMembers(childComplexity, args["params"].(model.InviteMemberInput)), true
case "Mutation.login": case "Mutation.login":
if e.complexity.Mutation.Login == nil { if e.complexity.Mutation.Login == nil {
break break
@@ -711,6 +729,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.ResetPassword(childComplexity, args["params"].(model.ResetPasswordInput)), true return e.complexity.Mutation.ResetPassword(childComplexity, args["params"].(model.ResetPasswordInput)), true
case "Mutation.revoke":
if e.complexity.Mutation.Revoke == nil {
break
}
args, err := ec.field_Mutation_revoke_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.Revoke(childComplexity, args["params"].(model.OAuthRevokeInput)), true
case "Mutation.signup": case "Mutation.signup":
if e.complexity.Mutation.Signup == nil { if e.complexity.Mutation.Signup == nil {
break break
@@ -1038,6 +1068,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.VerificationRequest.Identifier(childComplexity), true return e.complexity.VerificationRequest.Identifier(childComplexity), true
case "VerificationRequest.nonce":
if e.complexity.VerificationRequest.Nonce == nil {
break
}
return e.complexity.VerificationRequest.Nonce(childComplexity), true
case "VerificationRequest.redirect_uri":
if e.complexity.VerificationRequest.RedirectURI == nil {
break
}
return e.complexity.VerificationRequest.RedirectURI(childComplexity), true
case "VerificationRequest.token": case "VerificationRequest.token":
if e.complexity.VerificationRequest.Token == nil { if e.complexity.VerificationRequest.Token == nil {
break break
@@ -1189,6 +1233,8 @@ type VerificationRequest {
expires: Int64 expires: Int64
created_at: Int64 created_at: Int64
updated_at: Int64 updated_at: Int64
nonce: String
redirect_uri: String
} }
type VerificationRequests { type VerificationRequests {
@@ -1311,6 +1357,7 @@ input SignUpInput {
password: String! password: String!
confirm_password: String! confirm_password: String!
roles: [String!] roles: [String!]
scope: [String!]
} }
input LoginInput { input LoginInput {
@@ -1361,6 +1408,8 @@ input UpdateUserInput {
input ForgotPasswordInput { input ForgotPasswordInput {
email: String! email: String!
state: String
redirect_uri: String
} }
input ResetPasswordInput { input ResetPasswordInput {
@@ -1377,6 +1426,8 @@ input MagicLinkLoginInput {
email: String! email: String!
roles: [String!] roles: [String!]
scope: [String!] scope: [String!]
state: String
redirect_uri: String
} }
input SessionQueryInput { input SessionQueryInput {
@@ -1393,6 +1444,15 @@ input PaginatedInput {
pagination: PaginationInput pagination: PaginationInput
} }
input OAuthRevokeInput {
refresh_token: String!
}
input InviteMemberInput {
emails: [String!]!
redirect_uri: String
}
type Mutation { type Mutation {
signup(params: SignUpInput!): AuthResponse! signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse! login(params: LoginInput!): AuthResponse!
@@ -1403,6 +1463,7 @@ type Mutation {
resend_verify_email(params: ResendVerifyEmailInput!): Response! resend_verify_email(params: ResendVerifyEmailInput!): Response!
forgot_password(params: ForgotPasswordInput!): Response! forgot_password(params: ForgotPasswordInput!): Response!
reset_password(params: ResetPasswordInput!): Response! reset_password(params: ResetPasswordInput!): Response!
revoke(params: OAuthRevokeInput!): Response!
# admin only apis # admin only apis
_delete_user(params: DeleteUserInput!): Response! _delete_user(params: DeleteUserInput!): Response!
_update_user(params: UpdateUserInput!): User! _update_user(params: UpdateUserInput!): User!
@@ -1410,6 +1471,7 @@ type Mutation {
_admin_login(params: AdminLoginInput!): Response! _admin_login(params: AdminLoginInput!): Response!
_admin_logout: Response! _admin_logout: Response!
_update_env(params: UpdateEnvInput!): Response! _update_env(params: UpdateEnvInput!): Response!
_invite_members(params: InviteMemberInput!): Response!
} }
type Query { type Query {
@@ -1475,6 +1537,21 @@ func (ec *executionContext) field_Mutation__delete_user_args(ctx context.Context
return args, nil return args, nil
} }
func (ec *executionContext) field_Mutation__invite_members_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 model.InviteMemberInput
if tmp, ok := rawArgs["params"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params"))
arg0, err = ec.unmarshalNInviteMemberInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐInviteMemberInput(ctx, tmp)
if err != nil {
return nil, err
}
}
args["params"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation__update_env_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Mutation__update_env_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@@ -1580,6 +1657,21 @@ func (ec *executionContext) field_Mutation_reset_password_args(ctx context.Conte
return args, nil return args, nil
} }
func (ec *executionContext) field_Mutation_revoke_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 model.OAuthRevokeInput
if tmp, ok := rawArgs["params"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params"))
arg0, err = ec.unmarshalNOAuthRevokeInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐOAuthRevokeInput(ctx, tmp)
if err != nil {
return nil, err
}
}
args["params"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_signup_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Mutation_signup_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@@ -3838,6 +3930,48 @@ func (ec *executionContext) _Mutation_reset_password(ctx context.Context, field
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
} }
func (ec *executionContext) _Mutation_revoke(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_revoke_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().Revoke(rctx, args["params"].(model.OAuthRevokeInput))
})
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.(*model.Response)
fc.Result = res
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation__delete_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Mutation__delete_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -4083,6 +4217,48 @@ func (ec *executionContext) _Mutation__update_env(ctx context.Context, field gra
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
} }
func (ec *executionContext) _Mutation__invite_members(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation__invite_members_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().InviteMembers(rctx, args["params"].(model.InviteMemberInput))
})
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.(*model.Response)
fc.Result = res
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
}
func (ec *executionContext) _Pagination_limit(ctx context.Context, field graphql.CollectedField, obj *model.Pagination) (ret graphql.Marshaler) { func (ec *executionContext) _Pagination_limit(ctx context.Context, field graphql.CollectedField, obj *model.Pagination) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -5451,6 +5627,70 @@ func (ec *executionContext) _VerificationRequest_updated_at(ctx context.Context,
return ec.marshalOInt642ᚖint64(ctx, field.Selections, res) return ec.marshalOInt642ᚖint64(ctx, field.Selections, res)
} }
func (ec *executionContext) _VerificationRequest_nonce(ctx context.Context, field graphql.CollectedField, obj *model.VerificationRequest) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "VerificationRequest",
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.Nonce, 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) _VerificationRequest_redirect_uri(ctx context.Context, field graphql.CollectedField, obj *model.VerificationRequest) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "VerificationRequest",
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.RedirectURI, 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) _VerificationRequests_pagination(ctx context.Context, field graphql.CollectedField, obj *model.VerificationRequests) (ret graphql.Marshaler) { func (ec *executionContext) _VerificationRequests_pagination(ctx context.Context, field graphql.CollectedField, obj *model.VerificationRequests) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -6729,6 +6969,53 @@ func (ec *executionContext) unmarshalInputForgotPasswordInput(ctx context.Contex
if err != nil { if err != nil {
return it, err return it, err
} }
case "state":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
it.State, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
case "redirect_uri":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("redirect_uri"))
it.RedirectURI, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputInviteMemberInput(ctx context.Context, obj interface{}) (model.InviteMemberInput, error) {
var it model.InviteMemberInput
asMap := map[string]interface{}{}
for k, v := range obj.(map[string]interface{}) {
asMap[k] = v
}
for k, v := range asMap {
switch k {
case "emails":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("emails"))
it.Emails, err = ec.unmarshalNString2ᚕstringᚄ(ctx, v)
if err != nil {
return it, err
}
case "redirect_uri":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("redirect_uri"))
it.RedirectURI, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
} }
} }
@@ -6815,6 +7102,45 @@ func (ec *executionContext) unmarshalInputMagicLinkLoginInput(ctx context.Contex
if err != nil { if err != nil {
return it, err return it, err
} }
case "state":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
it.State, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
case "redirect_uri":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("redirect_uri"))
it.RedirectURI, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputOAuthRevokeInput(ctx context.Context, obj interface{}) (model.OAuthRevokeInput, error) {
var it model.OAuthRevokeInput
asMap := map[string]interface{}{}
for k, v := range obj.(map[string]interface{}) {
asMap[k] = v
}
for k, v := range asMap {
switch k {
case "refresh_token":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("refresh_token"))
it.RefreshToken, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
} }
} }
@@ -7081,6 +7407,14 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj i
if err != nil { if err != nil {
return it, err return it, err
} }
case "scope":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("scope"))
it.Scope, err = ec.unmarshalOString2ᚕstringᚄ(ctx, v)
if err != nil {
return it, err
}
} }
} }
@@ -7921,6 +8255,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "revoke":
out.Values[i] = ec._Mutation_revoke(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "_delete_user": case "_delete_user":
out.Values[i] = ec._Mutation__delete_user(ctx, field) out.Values[i] = ec._Mutation__delete_user(ctx, field)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
@@ -7951,6 +8290,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "_invite_members":
out.Values[i] = ec._Mutation__invite_members(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@@ -8290,6 +8634,10 @@ func (ec *executionContext) _VerificationRequest(ctx context.Context, sel ast.Se
out.Values[i] = ec._VerificationRequest_created_at(ctx, field, obj) out.Values[i] = ec._VerificationRequest_created_at(ctx, field, obj)
case "updated_at": case "updated_at":
out.Values[i] = ec._VerificationRequest_updated_at(ctx, field, obj) out.Values[i] = ec._VerificationRequest_updated_at(ctx, field, obj)
case "nonce":
out.Values[i] = ec._VerificationRequest_nonce(ctx, field, obj)
case "redirect_uri":
out.Values[i] = ec._VerificationRequest_redirect_uri(ctx, field, obj)
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@@ -8676,6 +9024,11 @@ func (ec *executionContext) marshalNInt642int64(ctx context.Context, sel ast.Sel
return res return res
} }
func (ec *executionContext) unmarshalNInviteMemberInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐInviteMemberInput(ctx context.Context, v interface{}) (model.InviteMemberInput, error) {
res, err := ec.unmarshalInputInviteMemberInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) unmarshalNLoginInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐLoginInput(ctx context.Context, v interface{}) (model.LoginInput, error) { func (ec *executionContext) unmarshalNLoginInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐLoginInput(ctx context.Context, v interface{}) (model.LoginInput, error) {
res, err := ec.unmarshalInputLoginInput(ctx, v) res, err := ec.unmarshalInputLoginInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)
@@ -8700,6 +9053,11 @@ func (ec *executionContext) marshalNMeta2ᚖgithubᚗcomᚋauthorizerdevᚋautho
return ec._Meta(ctx, sel, v) return ec._Meta(ctx, sel, v)
} }
func (ec *executionContext) unmarshalNOAuthRevokeInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐOAuthRevokeInput(ctx context.Context, v interface{}) (model.OAuthRevokeInput, error) {
res, err := ec.unmarshalInputOAuthRevokeInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNPagination2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐPagination(ctx context.Context, sel ast.SelectionSet, v *model.Pagination) graphql.Marshaler { func (ec *executionContext) marshalNPagination2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐPagination(ctx context.Context, sel ast.SelectionSet, v *model.Pagination) graphql.Marshaler {
if v == nil { if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {

View File

@@ -70,6 +70,13 @@ type Error struct {
type ForgotPasswordInput struct { type ForgotPasswordInput struct {
Email string `json:"email"` Email string `json:"email"`
State *string `json:"state"`
RedirectURI *string `json:"redirect_uri"`
}
type InviteMemberInput struct {
Emails []string `json:"emails"`
RedirectURI *string `json:"redirect_uri"`
} }
type LoginInput struct { type LoginInput struct {
@@ -83,6 +90,8 @@ type MagicLinkLoginInput struct {
Email string `json:"email"` Email string `json:"email"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
Scope []string `json:"scope"` Scope []string `json:"scope"`
State *string `json:"state"`
RedirectURI *string `json:"redirect_uri"`
} }
type Meta struct { type Meta struct {
@@ -96,6 +105,10 @@ type Meta struct {
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"` IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
} }
type OAuthRevokeInput struct {
RefreshToken string `json:"refresh_token"`
}
type PaginatedInput struct { type PaginatedInput struct {
Pagination *PaginationInput `json:"pagination"` Pagination *PaginationInput `json:"pagination"`
} }
@@ -145,6 +158,7 @@ type SignUpInput struct {
Password string `json:"password"` Password string `json:"password"`
ConfirmPassword string `json:"confirm_password"` ConfirmPassword string `json:"confirm_password"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
Scope []string `json:"scope"`
} }
type UpdateEnvInput struct { type UpdateEnvInput struct {
@@ -246,6 +260,8 @@ type VerificationRequest struct {
Expires *int64 `json:"expires"` Expires *int64 `json:"expires"`
CreatedAt *int64 `json:"created_at"` CreatedAt *int64 `json:"created_at"`
UpdatedAt *int64 `json:"updated_at"` UpdatedAt *int64 `json:"updated_at"`
Nonce *string `json:"nonce"`
RedirectURI *string `json:"redirect_uri"`
} }
type VerificationRequests struct { type VerificationRequests struct {

View File

@@ -57,6 +57,8 @@ type VerificationRequest {
expires: Int64 expires: Int64
created_at: Int64 created_at: Int64
updated_at: Int64 updated_at: Int64
nonce: String
redirect_uri: String
} }
type VerificationRequests { type VerificationRequests {
@@ -179,6 +181,7 @@ input SignUpInput {
password: String! password: String!
confirm_password: String! confirm_password: String!
roles: [String!] roles: [String!]
scope: [String!]
} }
input LoginInput { input LoginInput {
@@ -229,6 +232,8 @@ input UpdateUserInput {
input ForgotPasswordInput { input ForgotPasswordInput {
email: String! email: String!
state: String
redirect_uri: String
} }
input ResetPasswordInput { input ResetPasswordInput {
@@ -245,6 +250,8 @@ input MagicLinkLoginInput {
email: String! email: String!
roles: [String!] roles: [String!]
scope: [String!] scope: [String!]
state: String
redirect_uri: String
} }
input SessionQueryInput { input SessionQueryInput {
@@ -261,6 +268,15 @@ input PaginatedInput {
pagination: PaginationInput pagination: PaginationInput
} }
input OAuthRevokeInput {
refresh_token: String!
}
input InviteMemberInput {
emails: [String!]!
redirect_uri: String
}
type Mutation { type Mutation {
signup(params: SignUpInput!): AuthResponse! signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse! login(params: LoginInput!): AuthResponse!
@@ -271,6 +287,7 @@ type Mutation {
resend_verify_email(params: ResendVerifyEmailInput!): Response! resend_verify_email(params: ResendVerifyEmailInput!): Response!
forgot_password(params: ForgotPasswordInput!): Response! forgot_password(params: ForgotPasswordInput!): Response!
reset_password(params: ResetPasswordInput!): Response! reset_password(params: ResetPasswordInput!): Response!
revoke(params: OAuthRevokeInput!): Response!
# admin only apis # admin only apis
_delete_user(params: DeleteUserInput!): Response! _delete_user(params: DeleteUserInput!): Response!
_update_user(params: UpdateUserInput!): User! _update_user(params: UpdateUserInput!): User!
@@ -278,6 +295,7 @@ type Mutation {
_admin_login(params: AdminLoginInput!): Response! _admin_login(params: AdminLoginInput!): Response!
_admin_logout: Response! _admin_logout: Response!
_update_env(params: UpdateEnvInput!): Response! _update_env(params: UpdateEnvInput!): Response!
_invite_members(params: InviteMemberInput!): Response!
} }
type Query { type Query {

View File

@@ -47,6 +47,10 @@ func (r *mutationResolver) ResetPassword(ctx context.Context, params model.Reset
return resolvers.ResetPasswordResolver(ctx, params) return resolvers.ResetPasswordResolver(ctx, params)
} }
func (r *mutationResolver) Revoke(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) {
return resolvers.RevokeResolver(ctx, params)
}
func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) { func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) {
return resolvers.DeleteUserResolver(ctx, params) return resolvers.DeleteUserResolver(ctx, params)
} }
@@ -71,6 +75,10 @@ func (r *mutationResolver) UpdateEnv(ctx context.Context, params model.UpdateEnv
return resolvers.UpdateEnvResolver(ctx, params) return resolvers.UpdateEnvResolver(ctx, params)
} }
func (r *mutationResolver) InviteMembers(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) {
return resolvers.InviteMembersResolver(ctx, params)
}
func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) { func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) {
return resolvers.MetaResolver(ctx) return resolvers.MetaResolver(ctx)
} }

View File

@@ -1,13 +1,11 @@
package handlers package handlers
import ( import (
"encoding/json"
"log" "log"
"net/http" "net/http"
"strings" "strings"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/crypto"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -29,44 +27,25 @@ func AppHandler() gin.HandlerFunc {
return return
} }
state := c.Query("state") redirect_uri := strings.TrimSpace(c.Query("redirect_uri"))
state := strings.TrimSpace(c.Query("state"))
scopeString := strings.TrimSpace(c.Query("scope"))
var stateObj State var scope []string
if scopeString == "" {
if state == "" { scope = []string{"openid", "profile", "email"}
stateObj.AuthorizerURL = hostname
stateObj.RedirectURL = hostname + "/app"
} else { } else {
decodedState, err := crypto.DecryptB64(state) scope = strings.Split(scopeString, " ")
if err != nil {
c.JSON(400, gin.H{"error": "[unable to decode state] invalid state"})
return
} }
err = json.Unmarshal([]byte(decodedState), &stateObj) if redirect_uri == "" {
if err != nil { redirect_uri = hostname + "/app"
c.JSON(400, gin.H{"error": "[unable to parse state] invalid state"}) } else {
return
}
stateObj.AuthorizerURL = strings.TrimSuffix(stateObj.AuthorizerURL, "/")
stateObj.RedirectURL = strings.TrimSuffix(stateObj.RedirectURL, "/")
// validate redirect url with allowed origins // validate redirect url with allowed origins
if !utils.IsValidOrigin(stateObj.RedirectURL) { if !utils.IsValidOrigin(redirect_uri) {
c.JSON(400, gin.H{"error": "invalid redirect url"}) c.JSON(400, gin.H{"error": "invalid redirect url"})
return return
} }
if stateObj.AuthorizerURL == "" {
c.JSON(400, gin.H{"error": "invalid authorizer url"})
return
}
// validate host and domain of authorizer url
if strings.TrimSuffix(stateObj.AuthorizerURL, "/") != hostname {
c.JSON(400, gin.H{"error": "invalid host url"})
return
}
} }
// debug the request state // debug the request state
@@ -77,9 +56,11 @@ func AppHandler() gin.HandlerFunc {
} }
} }
c.HTML(http.StatusOK, "app.tmpl", gin.H{ c.HTML(http.StatusOK, "app.tmpl", gin.H{
"data": map[string]string{ "data": map[string]interface{}{
"authorizerURL": stateObj.AuthorizerURL, "authorizerURL": hostname,
"redirectURL": stateObj.RedirectURL, "redirectURL": redirect_uri,
"scope": scope,
"state": state,
"organizationName": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName), "organizationName": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName),
"organizationLogo": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo), "organizationLogo": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo),
}, },

View File

@@ -2,6 +2,7 @@ package handlers
import ( import (
"net/http" "net/http"
"strconv"
"strings" "strings"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
@@ -17,6 +18,7 @@ import (
// AuthorizeHandler is the handler for the /authorize route // AuthorizeHandler is the handler for the /authorize route
// required params // required params
// ?redirect_uri = redirect url // ?redirect_uri = redirect url
// ?response_mode = to decide if result should be html or re-direct
// state[recommended] = to prevent CSRF attack (for authorizer its compulsory) // state[recommended] = to prevent CSRF attack (for authorizer its compulsory)
// code_challenge = to prevent CSRF attack // code_challenge = to prevent CSRF attack
// code_challenge_method = to prevent CSRF attack [only sh256 is supported] // code_challenge_method = to prevent CSRF attack [only sh256 is supported]
@@ -31,8 +33,35 @@ func AuthorizeHandler() gin.HandlerFunc {
scopeString := strings.TrimSpace(gc.Query("scope")) scopeString := strings.TrimSpace(gc.Query("scope"))
clientID := strings.TrimSpace(gc.Query("client_id")) clientID := strings.TrimSpace(gc.Query("client_id"))
template := "authorize.tmpl" template := "authorize.tmpl"
responseMode := strings.TrimSpace(gc.Query("response_mode"))
var scope []string
if scopeString == "" {
scope = []string{"openid", "profile", "email"}
} else {
scope = strings.Split(scopeString, " ")
}
if responseMode == "" {
responseMode = "query"
}
if responseMode != "query" && responseMode != "web_message" {
gc.JSON(400, gin.H{"error": "invalid response mode"})
}
if redirectURI == "" {
redirectURI = "/app"
}
isQuery := responseMode == "query"
loginURL := "/app?state=" + state + "&scope=" + strings.Join(scope, " ") + "&redirect_uri=" + redirectURI
if clientID == "" { if clientID == "" {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
gc.HTML(http.StatusOK, template, gin.H{ gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI, "target_origin": redirectURI,
"authorization_response": map[string]interface{}{ "authorization_response": map[string]interface{}{
@@ -42,10 +71,14 @@ func AuthorizeHandler() gin.HandlerFunc {
}, },
}, },
}) })
}
return return
} }
if clientID != envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) { if clientID != envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
gc.HTML(http.StatusOK, template, gin.H{ gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI, "target_origin": redirectURI,
"authorization_response": map[string]interface{}{ "authorization_response": map[string]interface{}{
@@ -55,23 +88,14 @@ func AuthorizeHandler() gin.HandlerFunc {
}, },
}, },
}) })
return
} }
if redirectURI == "" {
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "redirect_uri is required",
},
},
})
return return
} }
if state == "" { if state == "" {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
gc.HTML(http.StatusOK, template, gin.H{ gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI, "target_origin": redirectURI,
"authorization_response": map[string]interface{}{ "authorization_response": map[string]interface{}{
@@ -81,6 +105,7 @@ func AuthorizeHandler() gin.HandlerFunc {
}, },
}, },
}) })
}
return return
} }
@@ -88,17 +113,13 @@ func AuthorizeHandler() gin.HandlerFunc {
responseType = "token" responseType = "token"
} }
var scope []string
if scopeString == "" {
scope = []string{"openid", "profile", "email"}
} else {
scope = strings.Split(scopeString, " ")
}
isResponseTypeCode := responseType == "code" isResponseTypeCode := responseType == "code"
isResponseTypeToken := responseType == "token" isResponseTypeToken := responseType == "token"
if !isResponseTypeCode && !isResponseTypeToken { if !isResponseTypeCode && !isResponseTypeToken {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
gc.HTML(http.StatusOK, template, gin.H{ gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI, "target_origin": redirectURI,
"authorization_response": map[string]interface{}{ "authorization_response": map[string]interface{}{
@@ -108,11 +129,15 @@ func AuthorizeHandler() gin.HandlerFunc {
}, },
}, },
}) })
}
return return
} }
if isResponseTypeCode { if isResponseTypeCode {
if codeChallenge == "" { if codeChallenge == "" {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
gc.HTML(http.StatusBadRequest, template, gin.H{ gc.HTML(http.StatusBadRequest, template, gin.H{
"target_origin": redirectURI, "target_origin": redirectURI,
"authorization_response": map[string]interface{}{ "authorization_response": map[string]interface{}{
@@ -122,12 +147,16 @@ func AuthorizeHandler() gin.HandlerFunc {
}, },
}, },
}) })
}
return return
} }
} }
sessionToken, err := cookie.GetSession(gc) sessionToken, err := cookie.GetSession(gc)
if err != nil { if err != nil {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
gc.HTML(http.StatusOK, template, gin.H{ gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI, "target_origin": redirectURI,
"authorization_response": map[string]interface{}{ "authorization_response": map[string]interface{}{
@@ -138,12 +167,16 @@ func AuthorizeHandler() gin.HandlerFunc {
}, },
}, },
}) })
}
return return
} }
// get session from cookie // get session from cookie
claims, err := token.ValidateBrowserSession(gc, sessionToken) claims, err := token.ValidateBrowserSession(gc, sessionToken)
if err != nil { if err != nil {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
gc.HTML(http.StatusOK, template, gin.H{ gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI, "target_origin": redirectURI,
"authorization_response": map[string]interface{}{ "authorization_response": map[string]interface{}{
@@ -154,11 +187,15 @@ func AuthorizeHandler() gin.HandlerFunc {
}, },
}, },
}) })
}
return return
} }
userID := claims.Subject userID := claims.Subject
user, err := db.Provider.GetUserByID(userID) user, err := db.Provider.GetUserByID(userID)
if err != nil { if err != nil {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
gc.HTML(http.StatusOK, template, gin.H{ gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI, "target_origin": redirectURI,
"authorization_response": map[string]interface{}{ "authorization_response": map[string]interface{}{
@@ -169,6 +206,7 @@ func AuthorizeHandler() gin.HandlerFunc {
}, },
}, },
}) })
}
return return
} }
@@ -180,6 +218,9 @@ func AuthorizeHandler() gin.HandlerFunc {
nonce := uuid.New().String() nonce := uuid.New().String()
newSessionTokenData, newSessionToken, err := token.CreateSessionToken(user, nonce, claims.Roles, scope) newSessionTokenData, newSessionToken, err := token.CreateSessionToken(user, nonce, claims.Roles, scope)
if err != nil { if err != nil {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
gc.HTML(http.StatusOK, template, gin.H{ gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI, "target_origin": redirectURI,
"authorization_response": map[string]interface{}{ "authorization_response": map[string]interface{}{
@@ -190,6 +231,7 @@ func AuthorizeHandler() gin.HandlerFunc {
}, },
}, },
}) })
}
return return
} }
@@ -214,6 +256,9 @@ func AuthorizeHandler() gin.HandlerFunc {
// rollover the session for security // rollover the session for security
authToken, err := token.CreateAuthToken(gc, user, claims.Roles, scope) authToken, err := token.CreateAuthToken(gc, user, claims.Roles, scope)
if err != nil { if err != nil {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
gc.HTML(http.StatusOK, template, gin.H{ gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI, "target_origin": redirectURI,
"authorization_response": map[string]interface{}{ "authorization_response": map[string]interface{}{
@@ -224,14 +269,18 @@ func AuthorizeHandler() gin.HandlerFunc {
}, },
}, },
}) })
}
return return
} }
sessionstore.RemoveState(sessionToken) sessionstore.RemoveState(sessionToken)
sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID)
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
cookie.SetSession(gc, authToken.FingerPrintHash) cookie.SetSession(gc, authToken.FingerPrintHash)
expiresIn := int64(1800) expiresIn := int64(1800)
// used of query mode
params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token
res := map[string]interface{}{ res := map[string]interface{}{
"access_token": authToken.AccessToken.Token, "access_token": authToken.AccessToken.Token,
"id_token": authToken.IDToken.Token, "id_token": authToken.IDToken.Token,
@@ -243,9 +292,17 @@ func AuthorizeHandler() gin.HandlerFunc {
if authToken.RefreshToken != nil { if authToken.RefreshToken != nil {
res["refresh_token"] = authToken.RefreshToken.Token res["refresh_token"] = authToken.RefreshToken.Token
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) params += "&refresh_token=" + authToken.RefreshToken.Token
sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
} }
if isQuery {
if strings.Contains(redirectURI, "?") {
gc.Redirect(http.StatusFound, redirectURI+"&"+params)
} else {
gc.Redirect(http.StatusFound, redirectURI+"?"+params)
}
} else {
gc.HTML(http.StatusOK, template, gin.H{ gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI, "target_origin": redirectURI,
"authorization_response": map[string]interface{}{ "authorization_response": map[string]interface{}{
@@ -253,9 +310,13 @@ func AuthorizeHandler() gin.HandlerFunc {
"response": res, "response": res,
}, },
}) })
}
return return
} }
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
// by default return with error // by default return with error
gc.HTML(http.StatusOK, template, gin.H{ gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI, "target_origin": redirectURI,
@@ -268,4 +329,5 @@ func AuthorizeHandler() gin.HandlerFunc {
}, },
}) })
} }
}
} }

View File

@@ -2,6 +2,7 @@ package handlers
import ( import (
"net/http" "net/http"
"strings"
"github.com/authorizerdev/authorizer/server/cookie" "github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/crypto" "github.com/authorizerdev/authorizer/server/crypto"
@@ -9,8 +10,10 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// Handler to logout user
func LogoutHandler() gin.HandlerFunc { func LogoutHandler() gin.HandlerFunc {
return func(gc *gin.Context) { return func(gc *gin.Context) {
redirectURL := strings.TrimSpace(gc.Query("redirect_uri"))
// get fingerprint hash // get fingerprint hash
fingerprintHash, err := cookie.GetSession(gc) fingerprintHash, err := cookie.GetSession(gc)
if err != nil { if err != nil {
@@ -33,8 +36,12 @@ func LogoutHandler() gin.HandlerFunc {
sessionstore.RemoveState(fingerPrint) sessionstore.RemoveState(fingerPrint)
cookie.DeleteSession(gc) cookie.DeleteSession(gc)
if redirectURL != "" {
gc.Redirect(http.StatusFound, redirectURL)
} else {
gc.JSON(http.StatusOK, gin.H{ gc.JSON(http.StatusOK, gin.H{
"message": "Logged out successfully", "message": "Logged out successfully",
}) })
} }
}
} }

View File

@@ -7,6 +7,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"strconv"
"strings" "strings"
"time" "time"
@@ -21,7 +22,6 @@ import (
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@@ -39,14 +39,15 @@ func OAuthCallbackHandler() gin.HandlerFunc {
// contains random token, redirect url, role // contains random token, redirect url, role
sessionSplit := strings.Split(state, "___") sessionSplit := strings.Split(state, "___")
// TODO validate redirect url if len(sessionSplit) < 3 {
if len(sessionSplit) < 2 {
c.JSON(400, gin.H{"error": "invalid redirect url"}) c.JSON(400, gin.H{"error": "invalid redirect url"})
return return
} }
inputRoles := strings.Split(sessionSplit[2], ",") stateValue := sessionSplit[0]
redirectURL := sessionSplit[1] redirectURL := sessionSplit[1]
inputRoles := strings.Split(sessionSplit[2], ",")
scopes := strings.Split(sessionSplit[3], ",")
var err error var err error
user := models.User{} user := models.User{}
@@ -145,17 +146,29 @@ func OAuthCallbackHandler() gin.HandlerFunc {
} }
} }
// TODO use query param authToken, err := token.CreateAuthToken(c, user, inputRoles, scopes)
scope := []string{"openid", "email", "profile"}
nonce := uuid.New().String()
_, newSessionToken, err := token.CreateSessionToken(user, nonce, inputRoles, scope)
if err != nil { if err != nil {
c.JSON(500, gin.H{"error": err.Error()}) c.JSON(500, gin.H{"error": err.Error()})
} }
expiresIn := int64(1800)
params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + stateValue + "&id_token=" + authToken.IDToken.Token
cookie.SetSession(c, authToken.FingerPrintHash)
sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID)
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
if authToken.RefreshToken != nil {
params = params + `&refresh_token=` + authToken.RefreshToken.Token
sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
}
sessionstore.SetState(newSessionToken, nonce+"@"+user.ID)
cookie.SetSession(c, newSessionToken)
go utils.SaveSessionInDB(c, user.ID) go utils.SaveSessionInDB(c, user.ID)
if strings.Contains(redirectURL, "?") {
redirectURL = redirectURL + "&" + params
} else {
redirectURL = redirectURL + "?" + params
}
c.Redirect(http.StatusTemporaryRedirect, redirectURL) c.Redirect(http.StatusTemporaryRedirect, redirectURL)
} }
} }

View File

@@ -10,23 +10,38 @@ import (
"github.com/authorizerdev/authorizer/server/sessionstore" "github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
) )
// OAuthLoginHandler set host in the oauth state that is useful for redirecting to oauth_callback // OAuthLoginHandler set host in the oauth state that is useful for redirecting to oauth_callback
func OAuthLoginHandler() gin.HandlerFunc { func OAuthLoginHandler() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
hostname := utils.GetHost(c) hostname := utils.GetHost(c)
redirectURL := c.Query("redirectURL") redirectURI := strings.TrimSpace(c.Query("redirectURL"))
roles := c.Query("roles") roles := strings.TrimSpace(c.Query("roles"))
state := strings.TrimSpace(c.Query("state"))
scopeString := strings.TrimSpace(c.Query("scope"))
if redirectURL == "" { if redirectURI == "" {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"error": "invalid redirect url", "error": "invalid redirect uri",
}) })
return return
} }
if state == "" {
c.JSON(400, gin.H{
"error": "invalid state",
})
return
}
var scope []string
if scopeString == "" {
scope = []string{"openid", "profile", "email"}
} else {
scope = strings.Split(scopeString, " ")
}
if roles != "" { if roles != "" {
// validate role // validate role
rolesSplit := strings.Split(roles, ",") rolesSplit := strings.Split(roles, ",")
@@ -43,8 +58,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
roles = strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",") roles = strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",")
} }
uuid := uuid.New() oauthStateString := state + "___" + redirectURI + "___" + roles + "___" + strings.Join(scope, ",")
oauthStateString := uuid.String() + "___" + redirectURL + "___" + roles
provider := c.Param("oauth_provider") provider := c.Param("oauth_provider")
isProviderConfigured := true isProviderConfigured := true

50
server/handlers/revoke.go Normal file
View File

@@ -0,0 +1,50 @@
package handlers
import (
"net/http"
"strings"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/gin-gonic/gin"
)
// Revoke handler to revoke refresh token
func RevokeHandler() gin.HandlerFunc {
return func(gc *gin.Context) {
var reqBody map[string]string
if err := gc.BindJSON(&reqBody); err != nil {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "error_binding_json",
"error_description": err.Error(),
})
return
}
// get fingerprint hash
refreshToken := strings.TrimSpace(reqBody["refresh_token"])
clientID := strings.TrimSpace(reqBody["client_id"])
if clientID == "" {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "client_id_required",
"error_description": "The client id is required",
})
return
}
if clientID != envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_client_id",
"error_description": "The client id is invalid",
})
return
}
sessionstore.RemoveState(refreshToken)
gc.JSON(http.StatusOK, gin.H{
"message": "Token revoked successfully",
})
}
}

View File

@@ -15,6 +15,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// TokenHandler to handle /oauth/token requests
// grant type required
func TokenHandler() gin.HandlerFunc { func TokenHandler() gin.HandlerFunc {
return func(gc *gin.Context) { return func(gc *gin.Context) {
var reqBody map[string]string var reqBody map[string]string
@@ -29,6 +31,22 @@ func TokenHandler() gin.HandlerFunc {
codeVerifier := strings.TrimSpace(reqBody["code_verifier"]) codeVerifier := strings.TrimSpace(reqBody["code_verifier"])
code := strings.TrimSpace(reqBody["code"]) code := strings.TrimSpace(reqBody["code"])
clientID := strings.TrimSpace(reqBody["client_id"]) clientID := strings.TrimSpace(reqBody["client_id"])
grantType := strings.TrimSpace(reqBody["grant_type"])
refreshToken := strings.TrimSpace(reqBody["refresh_token"])
if grantType == "" {
grantType = "authorization_code"
}
isRefreshTokenGrant := grantType == "refresh_token"
isAuthorizationCodeGrant := grantType == "authorization_code"
if !isRefreshTokenGrant && !isAuthorizationCodeGrant {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_grant_type",
"error_description": "grant_type is invalid",
})
}
if clientID == "" { if clientID == "" {
gc.JSON(http.StatusBadRequest, gin.H{ gc.JSON(http.StatusBadRequest, gin.H{
@@ -46,6 +64,10 @@ func TokenHandler() gin.HandlerFunc {
return return
} }
var userID string
var roles, scope []string
if isAuthorizationCodeGrant {
if codeVerifier == "" { if codeVerifier == "" {
gc.JSON(http.StatusBadRequest, gin.H{ gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code_verifier", "error": "invalid_code_verifier",
@@ -88,6 +110,8 @@ func TokenHandler() gin.HandlerFunc {
return return
} }
// rollover the session for security
sessionstore.RemoveState(sessionDataSplit[1])
// validate session // validate session
claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1]) claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1])
if err != nil { if err != nil {
@@ -97,7 +121,38 @@ func TokenHandler() gin.HandlerFunc {
}) })
return return
} }
userID := claims.Subject userID = claims.Subject
roles = claims.Roles
scope = claims.Scope
} else {
// validate refresh token
if refreshToken == "" {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_refresh_token",
"error_description": "The refresh token is invalid",
})
}
claims, err := token.ValidateRefreshToken(gc, refreshToken)
if err != nil {
gc.JSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
"error_description": err.Error(),
})
}
userID = claims["sub"].(string)
rolesInterface := claims["roles"].([]interface{})
scopeInterface := claims["scope"].([]interface{})
for _, v := range rolesInterface {
roles = append(roles, v.(string))
}
for _, v := range scopeInterface {
scope = append(scope, v.(string))
}
// remove older refresh token and rotate it for security
sessionstore.RemoveState(refreshToken)
}
user, err := db.Provider.GetUserByID(userID) user, err := db.Provider.GetUserByID(userID)
if err != nil { if err != nil {
gc.JSON(http.StatusUnauthorized, gin.H{ gc.JSON(http.StatusUnauthorized, gin.H{
@@ -106,9 +161,8 @@ func TokenHandler() gin.HandlerFunc {
}) })
return return
} }
// rollover the session for security
sessionstore.RemoveState(sessionDataSplit[1]) authToken, err := token.CreateAuthToken(gc, user, roles, scope)
authToken, err := token.CreateAuthToken(gc, user, claims.Roles, claims.Scope)
if err != nil { if err != nil {
gc.JSON(http.StatusUnauthorized, gin.H{ gc.JSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized", "error": "unauthorized",
@@ -124,13 +178,14 @@ func TokenHandler() gin.HandlerFunc {
res := map[string]interface{}{ res := map[string]interface{}{
"access_token": authToken.AccessToken.Token, "access_token": authToken.AccessToken.Token,
"id_token": authToken.IDToken.Token, "id_token": authToken.IDToken.Token,
"scope": claims.Scope, "scope": scope,
"roles": roles,
"expires_in": expiresIn, "expires_in": expiresIn,
} }
if authToken.RefreshToken != nil { if authToken.RefreshToken != nil {
res["refresh_token"] = authToken.RefreshToken.Token res["refresh_token"] = authToken.RefreshToken.Token
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
} }
gc.JSON(http.StatusOK, res) gc.JSON(http.StatusOK, res)

View File

@@ -2,6 +2,7 @@ package handlers
import ( import (
"net/http" "net/http"
"strconv"
"strings" "strings"
"time" "time"
@@ -11,7 +12,6 @@ import (
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
) )
// VerifyEmailHandler handles the verify email route. // VerifyEmailHandler handles the verify email route.
@@ -19,7 +19,7 @@ import (
func VerifyEmailHandler() gin.HandlerFunc { func VerifyEmailHandler() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
errorRes := gin.H{ errorRes := gin.H{
"error": "invalid token", "error": "invalid_token",
} }
tokenInQuery := c.Query("token") tokenInQuery := c.Query("token")
if tokenInQuery == "" { if tokenInQuery == "" {
@@ -29,30 +29,24 @@ func VerifyEmailHandler() gin.HandlerFunc {
verificationRequest, err := db.Provider.GetVerificationRequestByToken(tokenInQuery) verificationRequest, err := db.Provider.GetVerificationRequestByToken(tokenInQuery)
if err != nil { if err != nil {
errorRes["error_description"] = err.Error()
c.JSON(400, errorRes) c.JSON(400, errorRes)
return return
} }
// verify if token exists in db // verify if token exists in db
hostname := utils.GetHost(c) hostname := utils.GetHost(c)
encryptedNonce, err := utils.EncryptNonce(verificationRequest.Nonce) claim, err := token.ParseJWTToken(tokenInQuery, hostname, verificationRequest.Nonce, verificationRequest.Email)
if err != nil {
c.JSON(400, gin.H{
"error": err.Error(),
})
return
}
claim, err := token.ParseJWTToken(tokenInQuery, hostname, encryptedNonce, verificationRequest.Email)
if err != nil { if err != nil {
errorRes["error_description"] = err.Error()
c.JSON(400, errorRes) c.JSON(400, errorRes)
return return
} }
user, err := db.Provider.GetUserByEmail(claim["sub"].(string)) user, err := db.Provider.GetUserByEmail(claim["sub"].(string))
if err != nil { if err != nil {
c.JSON(400, gin.H{ errorRes["error_description"] = err.Error()
"message": err.Error(), c.JSON(400, errorRes)
})
return return
} }
@@ -65,21 +59,53 @@ func VerifyEmailHandler() gin.HandlerFunc {
// delete from verification table // delete from verification table
db.Provider.DeleteVerificationRequest(verificationRequest) db.Provider.DeleteVerificationRequest(verificationRequest)
roles := strings.Split(user.Roles, ",") state := strings.TrimSpace(c.Query("state"))
scope := []string{"openid", "email", "profile"} redirectURL := strings.TrimSpace(c.Query("redirect_uri"))
nonce := uuid.New().String() rolesString := strings.TrimSpace(c.Query("roles"))
_, authToken, err := token.CreateSessionToken(user, nonce, roles, scope) var roles []string
if rolesString == "" {
roles = strings.Split(user.Roles, ",")
} else {
roles = strings.Split(rolesString, ",")
}
scopeString := strings.TrimSpace(c.Query("scope"))
var scope []string
if scopeString == "" {
scope = []string{"openid", "email", "profile"}
} else {
scope = strings.Split(scopeString, " ")
}
authToken, err := token.CreateAuthToken(c, user, roles, scope)
if err != nil { if err != nil {
c.JSON(400, gin.H{ errorRes["error_description"] = err.Error()
"message": err.Error(), c.JSON(500, errorRes)
})
return return
} }
sessionstore.SetState(authToken, nonce+"@"+user.ID) expiresIn := int64(1800)
cookie.SetSession(c, authToken) params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token
cookie.SetSession(c, authToken.FingerPrintHash)
sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID)
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
if authToken.RefreshToken != nil {
params = params + `&refresh_token=${refresh_token}`
sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
}
if redirectURL == "" {
redirectURL = claim["redirect_uri"].(string)
}
if strings.Contains(redirectURL, "?") {
redirectURL = redirectURL + "&" + params
} else {
redirectURL = redirectURL + "?" + params
}
go utils.SaveSessionInDB(c, user.ID) go utils.SaveSessionInDB(c, user.ID)
c.Redirect(http.StatusTemporaryRedirect, claim["redirect_url"].(string)) c.Redirect(http.StatusTemporaryRedirect, redirectURL)
} }
} }

View File

@@ -21,7 +21,8 @@ func main() {
envstore.ARG_ENV_FILE = flag.String("env_file", "", "Env file path") envstore.ARG_ENV_FILE = flag.String("env_file", "", "Env file path")
flag.Parse() flag.Parse()
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyVersion, VERSION) log.Println("=> version:", VERSION)
constants.VERSION = VERSION
// initialize required envs (mainly db & env file path) // initialize required envs (mainly db & env file path)
err := env.InitRequiredEnv() err := env.InitRequiredEnv()

View File

@@ -39,11 +39,16 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
} }
hostname := utils.GetHost(gc) hostname := utils.GetHost(gc)
nonce, nonceHash, err := utils.GenerateNonce() _, nonceHash, err := utils.GenerateNonce()
if err != nil { if err != nil {
return res, err return res, err
} }
verificationToken, err := token.CreateVerificationToken(params.Email, constants.VerificationTypeForgotPassword, hostname, nonceHash) redirectURL := utils.GetAppURL(gc) + "/reset-password"
if params.RedirectURI != nil {
redirectURL = *params.RedirectURI
}
verificationToken, err := token.CreateVerificationToken(params.Email, constants.VerificationTypeForgotPassword, hostname, nonceHash, redirectURL)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
@@ -52,7 +57,8 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
Identifier: constants.VerificationTypeForgotPassword, Identifier: constants.VerificationTypeForgotPassword,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email, Email: params.Email,
Nonce: nonce, Nonce: nonceHash,
RedirectURI: redirectURL,
}) })
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency

View File

@@ -0,0 +1,135 @@
package resolvers
import (
"context"
"errors"
"fmt"
"log"
"strings"
"time"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models"
emailservice "github.com/authorizerdev/authorizer/server/email"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils"
)
// InviteMembersResolver resolver to invite members
func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) {
gc, err := utils.GinContextFromContext(ctx)
if err != nil {
return nil, err
}
if !token.IsSuperAdmin(gc) {
return nil, errors.New("unauthorized")
}
// this feature is only allowed if email server is configured
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
return nil, errors.New("email sending is disabled")
}
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) && envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) {
return nil, errors.New("either basic authentication or magic link login is required")
}
// filter valid emails
emails := []string{}
for _, email := range params.Emails {
if utils.IsValidEmail(email) {
emails = append(emails, email)
}
}
if len(emails) == 0 {
return nil, errors.New("no valid emails found")
}
// TODO: optimise to use like query instead of looping through emails and getting user individually
// for each emails check if emails exists in db
newEmails := []string{}
for _, email := range emails {
_, err := db.Provider.GetUserByEmail(email)
if err != nil {
log.Printf("%s user not found. inviting user.", email)
newEmails = append(newEmails, email)
} else {
log.Println("%s user already exists. skipping.", email)
}
}
if len(newEmails) == 0 {
return nil, errors.New("all emails already exist")
}
// invite new emails
for _, email := range newEmails {
user := models.User{
Email: email,
Roles: strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ","),
}
hostname := utils.GetHost(gc)
verifyEmailURL := hostname + "/verify_email"
appURL := utils.GetAppURL(gc)
redirectURL := appURL
if params.RedirectURI != nil {
redirectURL = *params.RedirectURI
}
_, nonceHash, err := utils.GenerateNonce()
if err != nil {
return nil, err
}
verificationToken, err := token.CreateVerificationToken(email, constants.VerificationTypeForgotPassword, hostname, nonceHash, redirectURL)
if err != nil {
log.Println(`error generating token`, err)
}
verificationRequest := models.VerificationRequest{
Token: verificationToken,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: email,
Nonce: nonceHash,
RedirectURI: redirectURL,
}
// use magic link login if that option is on
if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) {
user.SignupMethods = constants.SignupMethodMagicLinkLogin
verificationRequest.Identifier = constants.VerificationTypeMagicLinkLogin
} else {
// use basic authentication if that option is on
user.SignupMethods = constants.SignupMethodBasicAuth
verificationRequest.Identifier = constants.VerificationTypeForgotPassword
verifyEmailURL = appURL + "/setup-password"
}
user, err = db.Provider.AddUser(user)
if err != nil {
log.Printf("error inviting user: %s, err: %v", email, err)
return nil, err
}
_, err = db.Provider.AddVerificationRequest(verificationRequest)
if err != nil {
log.Printf("error inviting user: %s, err: %v", email, err)
return nil, err
}
go emailservice.InviteEmail(email, verificationToken, verifyEmailURL)
}
return &model.Response{
Message: fmt.Sprintf("%d user(s) invited successfully.", len(newEmails)),
}, nil
}

View File

@@ -69,8 +69,6 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
return res, err return res, err
} }
cookie.SetSession(gc, authToken.FingerPrintHash)
expiresIn := int64(1800) expiresIn := int64(1800)
res = &model.AuthResponse{ res = &model.AuthResponse{
Message: `Logged in successfully`, Message: `Logged in successfully`,
@@ -80,12 +78,13 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
User: user.AsAPIUser(), User: user.AsAPIUser(),
} }
cookie.SetSession(gc, authToken.FingerPrintHash)
sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID)
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
if authToken.RefreshToken != nil { if authToken.RefreshToken != nil {
res.RefreshToken = &authToken.RefreshToken.Token res.RefreshToken = &authToken.RefreshToken.Token
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
} }
go utils.SaveSessionInDB(gc, user.ID) go utils.SaveSessionInDB(gc, user.ID)

View File

@@ -68,6 +68,9 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
// Need to modify roles in this case // Need to modify roles in this case
// find the unassigned roles // find the unassigned roles
if len(params.Roles) <= 0 {
inputRoles = envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles)
}
existingRoles := strings.Split(existingUser.Roles, ",") existingRoles := strings.Split(existingUser.Roles, ",")
unasignedRoles := []string{} unasignedRoles := []string{}
for _, ir := range inputRoles { for _, ir := range inputRoles {
@@ -109,24 +112,46 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
hostname := utils.GetHost(gc) hostname := utils.GetHost(gc)
if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) { if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
// insert verification request // insert verification request
nonce, nonceHash, err := utils.GenerateNonce() _, nonceHash, err := utils.GenerateNonce()
if err != nil { if err != nil {
return res, err return res, err
} }
redirectURLParams := "&roles=" + strings.Join(inputRoles, ",")
if params.State != nil {
redirectURLParams = redirectURLParams + "&state=" + *params.State
}
if params.Scope != nil && len(params.Scope) > 0 {
redirectURLParams = redirectURLParams + "&scope=" + strings.Join(params.Scope, " ")
}
redirectURL := utils.GetAppURL(gc)
if params.RedirectURI != nil {
redirectURL = *params.RedirectURI
}
if strings.Contains(redirectURL, "?") {
redirectURL = redirectURL + "&" + redirectURLParams
} else {
redirectURL = redirectURL + "?" + redirectURLParams
}
verificationType := constants.VerificationTypeMagicLinkLogin verificationType := constants.VerificationTypeMagicLinkLogin
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash) verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
db.Provider.AddVerificationRequest(models.VerificationRequest{ _, err = db.Provider.AddVerificationRequest(models.VerificationRequest{
Token: verificationToken, Token: verificationToken,
Identifier: verificationType, Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email, Email: params.Email,
Nonce: nonce, Nonce: nonceHash,
RedirectURI: redirectURL,
}) })
if err != nil {
return res, err
}
// exec it as go routin so that we can reduce the api latency // exec it as go routing so that we can reduce the api latency
go email.SendVerificationMail(params.Email, verificationToken, hostname) go email.SendVerificationMail(params.Email, verificationToken, hostname)
} }

View File

@@ -44,11 +44,12 @@ func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEma
} }
hostname := utils.GetHost(gc) hostname := utils.GetHost(gc)
nonce, nonceHash, err := utils.GenerateNonce() _, nonceHash, err := utils.GenerateNonce()
if err != nil { if err != nil {
return res, err return res, err
} }
verificationToken, err := token.CreateVerificationToken(params.Email, params.Identifier, hostname, nonceHash)
verificationToken, err := token.CreateVerificationToken(params.Email, params.Identifier, hostname, nonceHash, verificationRequest.RedirectURI)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
@@ -57,7 +58,8 @@ func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEma
Identifier: params.Identifier, Identifier: params.Identifier,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email, Email: params.Email,
Nonce: nonce, Nonce: nonceHash,
RedirectURI: verificationRequest.RedirectURI,
}) })
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency

View File

@@ -37,11 +37,7 @@ func ResetPasswordResolver(ctx context.Context, params model.ResetPasswordInput)
// verify if token exists in db // verify if token exists in db
hostname := utils.GetHost(gc) hostname := utils.GetHost(gc)
encryptedNonce, err := utils.EncryptNonce(verificationRequest.Nonce) claim, err := token.ParseJWTToken(params.Token, hostname, verificationRequest.Nonce, verificationRequest.Email)
if err != nil {
return res, err
}
claim, err := token.ParseJWTToken(params.Token, hostname, encryptedNonce, verificationRequest.Email)
if err != nil { if err != nil {
return res, fmt.Errorf(`invalid token`) return res, fmt.Errorf(`invalid token`)
} }

View File

@@ -0,0 +1,16 @@
package resolvers
import (
"context"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/sessionstore"
)
// RevokeResolver resolver to revoke refresh token
func RevokeResolver(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) {
sessionstore.RemoveState(params.RefreshToken)
return &model.Response{
Message: "Token revoked",
}, nil
}

View File

@@ -2,7 +2,9 @@ package resolvers
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log"
"github.com/authorizerdev/authorizer/server/cookie" "github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
@@ -24,13 +26,15 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod
sessionToken, err := cookie.GetSession(gc) sessionToken, err := cookie.GetSession(gc)
if err != nil { if err != nil {
return res, err log.Println("error getting session token:", err)
return res, errors.New("unauthorized")
} }
// get session from cookie // get session from cookie
claims, err := token.ValidateBrowserSession(gc, sessionToken) claims, err := token.ValidateBrowserSession(gc, sessionToken)
if err != nil { if err != nil {
return res, err log.Println("session validation failed:", err)
return res, errors.New("unauthorized")
} }
userID := claims.Subject userID := claims.Subject
user, err := db.Provider.GetUserByID(userID) user, err := db.Provider.GetUserByID(userID)
@@ -80,7 +84,7 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod
if authToken.RefreshToken != nil { if authToken.RefreshToken != nil {
res.RefreshToken = &authToken.RefreshToken.Token res.RefreshToken = &authToken.RefreshToken.Token
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
} }
return res, nil return res, nil

View File

@@ -123,12 +123,13 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
hostname := utils.GetHost(gc) hostname := utils.GetHost(gc)
if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) { if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
// insert verification request // insert verification request
nonce, nonceHash, err := utils.GenerateNonce() _, nonceHash, err := utils.GenerateNonce()
if err != nil { if err != nil {
return res, err return res, err
} }
verificationType := constants.VerificationTypeBasicAuthSignup verificationType := constants.VerificationTypeBasicAuthSignup
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash) redirectURL := utils.GetAppURL(gc)
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL)
if err != nil { if err != nil {
return res, err return res, err
} }
@@ -137,7 +138,8 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
Identifier: verificationType, Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email, Email: params.Email,
Nonce: nonce, Nonce: nonceHash,
RedirectURI: redirectURL,
}) })
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency
@@ -149,6 +151,9 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
} }
} else { } else {
scope := []string{"openid", "email", "profile"} scope := []string{"openid", "email", "profile"}
if params.Scope != nil && len(scope) > 0 {
scope = params.Scope
}
authToken, err := token.CreateAuthToken(gc, user, roles, scope) authToken, err := token.CreateAuthToken(gc, user, roles, scope)
if err != nil { if err != nil {

View File

@@ -129,12 +129,13 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
user.EmailVerifiedAt = nil user.EmailVerifiedAt = nil
hasEmailChanged = true hasEmailChanged = true
// insert verification request // insert verification request
nonce, nonceHash, err := utils.GenerateNonce() _, nonceHash, err := utils.GenerateNonce()
if err != nil { if err != nil {
return res, err return res, err
} }
verificationType := constants.VerificationTypeUpdateEmail verificationType := constants.VerificationTypeUpdateEmail
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash) redirectURL := utils.GetAppURL(gc)
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
@@ -143,7 +144,8 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
Identifier: verificationType, Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: newEmail, Email: newEmail,
Nonce: nonce, Nonce: nonceHash,
RedirectURI: redirectURL,
}) })
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency

View File

@@ -101,12 +101,13 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
user.Email = newEmail user.Email = newEmail
user.EmailVerifiedAt = nil user.EmailVerifiedAt = nil
// insert verification request // insert verification request
nonce, nonceHash, err := utils.GenerateNonce() _, nonceHash, err := utils.GenerateNonce()
if err != nil { if err != nil {
return res, err return res, err
} }
verificationType := constants.VerificationTypeUpdateEmail verificationType := constants.VerificationTypeUpdateEmail
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash) redirectURL := utils.GetAppURL(gc)
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
@@ -115,7 +116,8 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
Identifier: verificationType, Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: newEmail, Email: newEmail,
Nonce: nonce, Nonce: nonceHash,
RedirectURI: redirectURL,
}) })
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency

View File

@@ -29,11 +29,7 @@ func VerifyEmailResolver(ctx context.Context, params model.VerifyEmailInput) (*m
// verify if token exists in db // verify if token exists in db
hostname := utils.GetHost(gc) hostname := utils.GetHost(gc)
encryptedNonce, err := utils.EncryptNonce(verificationRequest.Nonce) claim, err := token.ParseJWTToken(params.Token, hostname, verificationRequest.Nonce, verificationRequest.Email)
if err != nil {
return res, err
}
claim, err := token.ParseJWTToken(params.Token, hostname, encryptedNonce, verificationRequest.Email)
if err != nil { if err != nil {
return res, fmt.Errorf(`invalid token: %s`, err.Error()) return res, fmt.Errorf(`invalid token: %s`, err.Error())
} }

View File

@@ -27,6 +27,7 @@ func InitRouter() *gin.Engine {
router.GET("/userinfo", handlers.UserInfoHandler()) router.GET("/userinfo", handlers.UserInfoHandler())
router.GET("/logout", handlers.LogoutHandler()) router.GET("/logout", handlers.LogoutHandler())
router.POST("/oauth/token", handlers.TokenHandler()) router.POST("/oauth/token", handlers.TokenHandler())
router.POST("/oauth/revoke", handlers.RevokeHandler())
router.LoadHTMLGlob("templates/*") router.LoadHTMLGlob("templates/*")
// login page app related routes. // login page app related routes.

View File

@@ -0,0 +1,58 @@
package test
import (
"fmt"
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/crypto"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func inviteUserTest(t *testing.T, s TestSetup) {
t.Helper()
t.Run(`should invite user successfully`, func(t *testing.T) {
req, ctx := createContext(s)
emails := []string{"invite_member1." + s.TestInfo.Email}
// unauthorized error
res, err := resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
Emails: emails,
})
assert.Error(t, err)
assert.Nil(t, res)
h, err := crypto.EncryptPassword(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
assert.Nil(t, err)
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h))
// invalid emails test
invalidEmailsTest := []string{
"test",
"test.com",
}
res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
Emails: invalidEmailsTest,
})
// valid test
res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
Emails: emails,
})
assert.Nil(t, err)
assert.NotNil(t, res)
// duplicate error test
res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
Emails: emails,
})
assert.Error(t, err)
assert.Nil(t, res)
cleanData(emails[0])
})
}

View File

@@ -15,7 +15,7 @@ func TestResolvers(t *testing.T) {
// constants.DbTypeArangodb: "http://localhost:8529", // constants.DbTypeArangodb: "http://localhost:8529",
// constants.DbTypeMongodb: "mongodb://localhost:27017", // constants.DbTypeMongodb: "mongodb://localhost:27017",
} }
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyVersion, "test")
for dbType, dbURL := range databases { for dbType, dbURL := range databases {
s := testSetup() s := testSetup()
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseURL, dbURL) envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseURL, dbURL)
@@ -62,6 +62,7 @@ func TestResolvers(t *testing.T) {
magicLinkLoginTests(t, s) magicLinkLoginTests(t, s)
logoutTests(t, s) logoutTests(t, s)
metaTests(t, s) metaTests(t, s)
inviteUserTest(t, s)
}) })
} }
} }

View File

@@ -91,7 +91,7 @@ func CreateAuthToken(gc *gin.Context, user models.User, roles, scope []string) (
} }
if utils.StringSliceContains(scope, "offline_access") { if utils.StringSliceContains(scope, "offline_access") {
refreshToken, refreshTokenExpiresAt, err := CreateRefreshToken(user, roles, hostname, nonce) refreshToken, refreshTokenExpiresAt, err := CreateRefreshToken(user, roles, scope, hostname, nonce)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -103,7 +103,7 @@ func CreateAuthToken(gc *gin.Context, user models.User, roles, scope []string) (
} }
// CreateRefreshToken util to create JWT token // CreateRefreshToken util to create JWT token
func CreateRefreshToken(user models.User, roles []string, hostname, nonce string) (string, int64, error) { func CreateRefreshToken(user models.User, roles, scopes []string, hostname, nonce string) (string, int64, error) {
// expires in 1 year // expires in 1 year
expiryBound := time.Hour * 8760 expiryBound := time.Hour * 8760
expiresAt := time.Now().Add(expiryBound).Unix() expiresAt := time.Now().Add(expiryBound).Unix()
@@ -115,6 +115,7 @@ func CreateRefreshToken(user models.User, roles []string, hostname, nonce string
"iat": time.Now().Unix(), "iat": time.Now().Unix(),
"token_type": constants.TokenTypeRefreshToken, "token_type": constants.TokenTypeRefreshToken,
"roles": roles, "roles": roles,
"scope": scopes,
"nonce": nonce, "nonce": nonce,
} }
@@ -198,6 +199,36 @@ func ValidateAccessToken(gc *gin.Context, accessToken string) (map[string]interf
return res, nil return res, nil
} }
// Function to validate refreshToken
func ValidateRefreshToken(gc *gin.Context, refreshToken string) (map[string]interface{}, error) {
var res map[string]interface{}
if refreshToken == "" {
return res, fmt.Errorf(`unauthorized`)
}
savedSession := sessionstore.GetState(refreshToken)
if savedSession == "" {
return res, fmt.Errorf(`unauthorized`)
}
savedSessionSplit := strings.Split(savedSession, "@")
nonce := savedSessionSplit[0]
userID := savedSessionSplit[1]
hostname := utils.GetHost(gc)
res, err := ParseJWTToken(refreshToken, hostname, nonce, userID)
if err != nil {
return res, err
}
if res["token_type"] != constants.TokenTypeRefreshToken {
return res, fmt.Errorf(`unauthorized: invalid token type`)
}
return res, nil
}
func ValidateBrowserSession(gc *gin.Context, encryptedSession string) (*SessionData, error) { func ValidateBrowserSession(gc *gin.Context, encryptedSession string) (*SessionData, error) {
if encryptedSession == "" { if encryptedSession == "" {
return nil, fmt.Errorf(`unauthorized`) return nil, fmt.Errorf(`unauthorized`)

View File

@@ -9,7 +9,7 @@ import (
) )
// CreateVerificationToken creates a verification JWT token // CreateVerificationToken creates a verification JWT token
func CreateVerificationToken(email, tokenType, hostname, nonceHash string) (string, error) { func CreateVerificationToken(email, tokenType, hostname, nonceHash, redirectURL string) (string, error) {
claims := jwt.MapClaims{ claims := jwt.MapClaims{
"iss": hostname, "iss": hostname,
"aud": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID), "aud": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID),
@@ -18,7 +18,7 @@ func CreateVerificationToken(email, tokenType, hostname, nonceHash string) (stri
"iat": time.Now().Unix(), "iat": time.Now().Unix(),
"token_type": tokenType, "token_type": tokenType,
"nonce": nonceHash, "nonce": nonceHash,
"redirect_url": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL), "redirect_uri": redirectURL,
} }
return SignJWTToken(claims) return SignJWTToken(claims)

View File

@@ -9,7 +9,7 @@ import (
// GetMeta helps in getting the meta data about the deployment from EnvData // GetMeta helps in getting the meta data about the deployment from EnvData
func GetMetaInfo() model.Meta { func GetMetaInfo() model.Meta {
return model.Meta{ return model.Meta{
Version: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyVersion), Version: constants.VERSION,
ClientID: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID), ClientID: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID),
IsGoogleLoginEnabled: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientID) != "" && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientSecret) != "", IsGoogleLoginEnabled: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientID) != "" && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientSecret) != "",
IsGithubLoginEnabled: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientID) != "" && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientSecret) != "", IsGithubLoginEnabled: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientID) != "" && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientSecret) != "",

View File

@@ -4,6 +4,8 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -71,3 +73,12 @@ func GetDomainName(uri string) string {
return host return host
} }
// GetAppURL to get /app/ url if not configured by user
func GetAppURL(gc *gin.Context) string {
envAppURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
if envAppURL == "" {
envAppURL = GetHost(gc) + "/app"
}
return envAppURL
}

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>{{.data.organizationName}}</title> <title>{{.data.organizationName}}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="apple-touch-icon" sizes="180x180" href="/app/favicon_io/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/app/favicon_io/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/app/favicon_io/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/app/favicon_io/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/app/favicon_io/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/app/favicon_io/favicon-16x16.png">

View File

@@ -8,7 +8,6 @@
(function (window, document) { (function (window, document) {
var targetOrigin = {{.target_origin}}; var targetOrigin = {{.target_origin}};
var authorizationResponse = {{.authorization_response}}; var authorizationResponse = {{.authorization_response}};
console.log({targetOrigin})
window.parent.postMessage(authorizationResponse, targetOrigin); window.parent.postMessage(authorizationResponse, targetOrigin);
})(this, this.document); })(this, this.document);
</script> </script>