Compare commits

...

10 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
30 changed files with 616 additions and 49 deletions

30
app/package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@authorizerdev/authorizer-react": "0.9.0-beta.3",
"@authorizerdev/authorizer-react": "latest",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17",
@@ -24,9 +24,9 @@
}
},
"node_modules/@authorizerdev/authorizer-js": {
"version": "0.4.0-beta.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.0.tgz",
"integrity": "sha512-wNh5ROldNqdbOXFPDlq1tObzPZyEQkbnOvSEwvnDfPYb9/BsJ3naj3/ayz4J2R5k2+Eyuk0LK64XYdkfIW0HYA==",
"version": "0.4.0-beta.3",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz",
"integrity": "sha512-OGZc6I6cnpi/WkSotkjVIc3LEzl8pFeiohr8+Db9xWd75/oTfOZqWRuIHTnTc1FC+6Sv2EjTJ9Aa6lrloWG+NQ==",
"dependencies": {
"node-fetch": "^2.6.1"
},
@@ -35,11 +35,11 @@
}
},
"node_modules/@authorizerdev/authorizer-react": {
"version": "0.9.0-beta.3",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.3.tgz",
"integrity": "sha512-P93PW6W3Qm9BW3160gn0Ce+64UCFAOpoEOHf5537LgFPE8LpNAIU3EI6EtMNkOJS58pu1h2UkfyRyX/j0Pohjw==",
"version": "0.9.0-beta.7",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz",
"integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==",
"dependencies": {
"@authorizerdev/authorizer-js": "^0.4.0-beta.0",
"@authorizerdev/authorizer-js": "^0.4.0-beta.3",
"final-form": "^4.20.2",
"react-final-form": "^6.5.3",
"styled-components": "^5.3.0"
@@ -829,19 +829,19 @@
},
"dependencies": {
"@authorizerdev/authorizer-js": {
"version": "0.4.0-beta.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.0.tgz",
"integrity": "sha512-wNh5ROldNqdbOXFPDlq1tObzPZyEQkbnOvSEwvnDfPYb9/BsJ3naj3/ayz4J2R5k2+Eyuk0LK64XYdkfIW0HYA==",
"version": "0.4.0-beta.3",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz",
"integrity": "sha512-OGZc6I6cnpi/WkSotkjVIc3LEzl8pFeiohr8+Db9xWd75/oTfOZqWRuIHTnTc1FC+6Sv2EjTJ9Aa6lrloWG+NQ==",
"requires": {
"node-fetch": "^2.6.1"
}
},
"@authorizerdev/authorizer-react": {
"version": "0.9.0-beta.3",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.3.tgz",
"integrity": "sha512-P93PW6W3Qm9BW3160gn0Ce+64UCFAOpoEOHf5537LgFPE8LpNAIU3EI6EtMNkOJS58pu1h2UkfyRyX/j0Pohjw==",
"version": "0.9.0-beta.7",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz",
"integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==",
"requires": {
"@authorizerdev/authorizer-js": "^0.4.0-beta.0",
"@authorizerdev/authorizer-js": "^0.4.0-beta.3",
"final-form": "^4.20.2",
"react-final-form": "^6.5.3",
"styled-components": "^5.3.0"

View File

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

View File

@@ -2,10 +2,33 @@ import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { AuthorizerProvider } from '@authorizerdev/authorizer-react';
import Root from './Root';
import { createRandomString } from './utils/common';
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
const globalState: Record<string, string> = window['__authorizer__'];
...window['__authorizer__'],
...urlProps,
};
return (
<div
style={{
@@ -38,7 +61,7 @@ export default function App() {
redirectURL: globalState.redirectURL,
}}
>
<Root />
<Root globalState={globalState} />
</AuthorizerProvider>
</BrowserRouter>
</div>

View File

@@ -1,19 +1,26 @@
import React, { useEffect, lazy, Suspense } from 'react';
import { Switch, Route } from 'react-router-dom';
import { useAuthorizer } from '@authorizerdev/authorizer-react';
import SetupPassword from './pages/setup-password';
const ResetPassword = lazy(() => import('./pages/rest-password'));
const Login = lazy(() => import('./pages/login'));
const Dashboard = lazy(() => import('./pages/dashboard'));
export default function Root() {
export default function Root({
globalState,
}: {
globalState: Record<string, string>;
}) {
const { token, loading, config } = useAuthorizer();
useEffect(() => {
if (token) {
console.log({ token });
let redirectURL = config.redirectURL || '/app';
const params = `access_token=${token.access_token}&id_token=${token.id_token}&expires_in=${token.expires_in}&refresh_token=${token.refresh_token}`;
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}`;
@@ -54,6 +61,9 @@ export default function Root() {
<Route path="/app/reset-password">
<ResetPassword />
</Route>
<Route path="/app/setup-password">
<SetupPassword />
</Route>
</Switch>
</Suspense>
);

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';
import { IconType } from 'react-icons';
import { ReactText } from 'react';
import { useMutation } from 'urql';
import { useMutation, useQuery } from 'urql';
import { NavLink, useNavigate, useLocation } from 'react-router-dom';
import { useAuthContext } from '../contexts/AuthContext';
import { AdminLogout } from '../graphql/mutation';
import { MetaQuery } from '../graphql/queries';
interface LinkItemProps {
name: string;
@@ -51,6 +52,7 @@ interface SidebarProps extends BoxProps {
export const Sidebar = ({ onClose, ...rest }: SidebarProps) => {
const { pathname } = useLocation();
const [{ fetching, data }] = useQuery({ query: MetaQuery });
return (
<Box
transition="3s ease"
@@ -98,6 +100,19 @@ export const Sidebar = ({ onClose, ...rest }: SidebarProps) => {
>
<NavItem icon={FiCode}>API Playground</NavItem>
</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>
);
};

View File

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

View File

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

View File

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

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,7 +1,7 @@
package email
import (
"fmt"
"log"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore"
@@ -107,7 +107,7 @@ func SendVerificationMail(toEmail, token, hostname string) error {
err := SendMail(Receiver, Subject, message)
if err != nil {
fmt.Println("=> error sending email:", err)
log.Println("=> error sending email:", err)
}
return err
}

View File

@@ -114,6 +114,7 @@ type ComplexityRoot struct {
AdminSignup func(childComplexity int, params model.AdminSignupInput) int
DeleteUser func(childComplexity int, params model.DeleteUserInput) 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
Logout func(childComplexity int) int
MagicLinkLogin func(childComplexity int, params model.MagicLinkLoginInput) int
@@ -208,6 +209,7 @@ type MutationResolver interface {
AdminLogin(ctx context.Context, params model.AdminLoginInput) (*model.Response, error)
AdminLogout(ctx context.Context) (*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 {
Meta(ctx context.Context) (*model.Meta, error)
@@ -660,6 +662,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
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":
if e.complexity.Mutation.Login == nil {
break
@@ -1343,6 +1357,7 @@ input SignUpInput {
password: String!
confirm_password: String!
roles: [String!]
scope: [String!]
}
input LoginInput {
@@ -1433,6 +1448,11 @@ input OAuthRevokeInput {
refresh_token: String!
}
input InviteMemberInput {
emails: [String!]!
redirect_uri: String
}
type Mutation {
signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse!
@@ -1451,6 +1471,7 @@ type Mutation {
_admin_login(params: AdminLoginInput!): Response!
_admin_logout: Response!
_update_env(params: UpdateEnvInput!): Response!
_invite_members(params: InviteMemberInput!): Response!
}
type Query {
@@ -1516,6 +1537,21 @@ func (ec *executionContext) field_Mutation__delete_user_args(ctx context.Context
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) {
var err error
args := map[string]interface{}{}
@@ -4181,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)
}
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) {
defer func() {
if r := recover(); r != nil {
@@ -6913,6 +6991,37 @@ func (ec *executionContext) unmarshalInputForgotPasswordInput(ctx context.Contex
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
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj interface{}) (model.LoginInput, error) {
var it model.LoginInput
asMap := map[string]interface{}{}
@@ -7298,6 +7407,14 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj i
if err != nil {
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
}
}
}
@@ -8173,6 +8290,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
invalids++
}
case "_invite_members":
out.Values[i] = ec._Mutation__invite_members(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@@ -8902,6 +9024,11 @@ func (ec *executionContext) marshalNInt642int64(ctx context.Context, sel ast.Sel
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) {
res, err := ec.unmarshalInputLoginInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)

View File

@@ -74,6 +74,11 @@ type ForgotPasswordInput struct {
RedirectURI *string `json:"redirect_uri"`
}
type InviteMemberInput struct {
Emails []string `json:"emails"`
RedirectURI *string `json:"redirect_uri"`
}
type LoginInput struct {
Email string `json:"email"`
Password string `json:"password"`
@@ -153,6 +158,7 @@ type SignUpInput struct {
Password string `json:"password"`
ConfirmPassword string `json:"confirm_password"`
Roles []string `json:"roles"`
Scope []string `json:"scope"`
}
type UpdateEnvInput struct {

View File

@@ -181,6 +181,7 @@ input SignUpInput {
password: String!
confirm_password: String!
roles: [String!]
scope: [String!]
}
input LoginInput {
@@ -271,6 +272,11 @@ input OAuthRevokeInput {
refresh_token: String!
}
input InviteMemberInput {
emails: [String!]!
redirect_uri: String
}
type Mutation {
signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse!
@@ -289,6 +295,7 @@ type Mutation {
_admin_login(params: AdminLoginInput!): Response!
_admin_logout: Response!
_update_env(params: UpdateEnvInput!): Response!
_invite_members(params: InviteMemberInput!): Response!
}
type Query {

View File

@@ -75,6 +75,10 @@ func (r *mutationResolver) UpdateEnv(ctx context.Context, params model.UpdateEnv
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) {
return resolvers.MetaResolver(ctx)
}
@@ -109,7 +113,5 @@ func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResol
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type (
mutationResolver struct{ *Resolver }
queryResolver struct{ *Resolver }
)
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

View File

@@ -21,7 +21,8 @@ func main() {
envstore.ARG_ENV_FILE = flag.String("env_file", "", "Env file path")
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)
err := env.InitRequiredEnv()

View File

@@ -43,7 +43,7 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
if err != nil {
return res, err
}
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
redirectURL := utils.GetAppURL(gc) + "/reset-password"
if params.RedirectURI != nil {
redirectURL = *params.RedirectURI
}

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

@@ -123,7 +123,7 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
if params.Scope != nil && len(params.Scope) > 0 {
redirectURLParams = redirectURLParams + "&scope=" + strings.Join(params.Scope, " ")
}
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
redirectURL := utils.GetAppURL(gc)
if params.RedirectURI != nil {
redirectURL = *params.RedirectURI
}

View File

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

View File

@@ -128,7 +128,7 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
return res, err
}
verificationType := constants.VerificationTypeBasicAuthSignup
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
redirectURL := utils.GetAppURL(gc)
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL)
if err != nil {
return res, err
@@ -151,6 +151,9 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
}
} else {
scope := []string{"openid", "email", "profile"}
if params.Scope != nil && len(scope) > 0 {
scope = params.Scope
}
authToken, err := token.CreateAuthToken(gc, user, roles, scope)
if err != nil {

View File

@@ -134,7 +134,7 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
return res, err
}
verificationType := constants.VerificationTypeUpdateEmail
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
redirectURL := utils.GetAppURL(gc)
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
if err != nil {
log.Println(`error generating token`, err)

View File

@@ -106,7 +106,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
return res, err
}
verificationType := constants.VerificationTypeUpdateEmail
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
redirectURL := utils.GetAppURL(gc)
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
if err != nil {
log.Println(`error generating token`, err)

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

View File

@@ -2,7 +2,6 @@ package token
import (
"errors"
"fmt"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/crypto"
@@ -92,7 +91,6 @@ func ParseJWTToken(token, hostname, nonce, subject string) (jwt.MapClaims, error
return claims, errors.New("invalid audience")
}
fmt.Println("claims:", claims["nonce"], nonce, claims["nonce"] == nonce)
if claims["nonce"] != nonce {
return claims, errors.New("invalid nonce")
}

View File

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

View File

@@ -4,6 +4,8 @@ import (
"net/url"
"strings"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/gin-gonic/gin"
)
@@ -71,3 +73,12 @@ func GetDomainName(uri string) string {
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
}