Compare commits

...

17 Commits

Author SHA1 Message Date
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
32 changed files with 494 additions and 106 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.9.0-beta.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.4.0-beta.0", "version": "0.4.0-beta.3",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz",
"integrity": "sha512-wNh5ROldNqdbOXFPDlq1tObzPZyEQkbnOvSEwvnDfPYb9/BsJ3naj3/ayz4J2R5k2+Eyuk0LK64XYdkfIW0HYA==", "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.9.0-beta.0", "version": "0.9.0-beta.7",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz",
"integrity": "sha512-+I0JGobxuTDwKPAqwkJuczXnQ+ooNnz9IrfQPl8ZHnKoZsRqSSPCOLO6o0BLBu65h6pWZdLEEFN8CjRnu1+Zuw==", "integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": "^0.4.0-beta.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.4.0-beta.0", "version": "0.4.0-beta.3",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz",
"integrity": "sha512-wNh5ROldNqdbOXFPDlq1tObzPZyEQkbnOvSEwvnDfPYb9/BsJ3naj3/ayz4J2R5k2+Eyuk0LK64XYdkfIW0HYA==", "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.9.0-beta.0", "version": "0.9.0-beta.7",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz",
"integrity": "sha512-+I0JGobxuTDwKPAqwkJuczXnQ+ooNnz9IrfQPl8ZHnKoZsRqSSPCOLO6o0BLBu65h6pWZdLEEFN8CjRnu1+Zuw==", "integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==",
"requires": { "requires": {
"@authorizerdev/authorizer-js": "^0.4.0-beta.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

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

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() {
// @ts-ignore const searchParams = new URLSearchParams(window.location.search);
const globalState: Record<string, string> = window['__authorizer__']; 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;
}
const globalState: Record<string, string> = {
// @ts-ignore
...window['__authorizer__'],
...urlProps,
};
return ( return (
<div <div
style={{ style={{
@@ -38,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

@@ -6,14 +6,20 @@ 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) {
console.log({ token });
let redirectURL = config.redirectURL || '/app'; 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); const url = new URL(redirectURL);
if (redirectURL.includes('?')) { if (redirectURL.includes('?')) {
redirectURL = `${redirectURL}&${params}`; redirectURL = `${redirectURL}&${params}`;

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>
<Box p="6" m="5" rounded="5" bg="white" w="500px" shadow="xl"> {fetching ? (
{children} <Spinner />
</Box> ) : (
<>
<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> </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

@@ -12,7 +12,7 @@ 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"` RedirectURI string `gorm:"type:text" json:"redirect_uri" bson:"redirect_uri"`
} }

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

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

@@ -119,6 +119,7 @@ type ComplexityRoot struct {
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
@@ -200,6 +201,7 @@ 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)
@@ -713,6 +715,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
@@ -1329,6 +1343,7 @@ input SignUpInput {
password: String! password: String!
confirm_password: String! confirm_password: String!
roles: [String!] roles: [String!]
scope: [String!]
} }
input LoginInput { input LoginInput {
@@ -1415,6 +1430,10 @@ input PaginatedInput {
pagination: PaginationInput pagination: PaginationInput
} }
input OAuthRevokeInput {
refresh_token: String!
}
type Mutation { type Mutation {
signup(params: SignUpInput!): AuthResponse! signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse! login(params: LoginInput!): AuthResponse!
@@ -1425,6 +1444,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!
@@ -1602,6 +1622,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{}{}
@@ -3860,6 +3895,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 {
@@ -6939,6 +7016,29 @@ func (ec *executionContext) unmarshalInputMagicLinkLoginInput(ctx context.Contex
return it, nil 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
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputPaginatedInput(ctx context.Context, obj interface{}) (model.PaginatedInput, error) { func (ec *executionContext) unmarshalInputPaginatedInput(ctx context.Context, obj interface{}) (model.PaginatedInput, error) {
var it model.PaginatedInput var it model.PaginatedInput
asMap := map[string]interface{}{} asMap := map[string]interface{}{}
@@ -7199,6 +7299,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
}
} }
} }
@@ -8039,6 +8147,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 {
@@ -8822,6 +8935,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

@@ -100,6 +100,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"`
} }
@@ -149,6 +153,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 {

View File

@@ -181,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 {
@@ -267,6 +268,10 @@ input PaginatedInput {
pagination: PaginationInput pagination: PaginationInput
} }
input OAuthRevokeInput {
refresh_token: String!
}
type Mutation { type Mutation {
signup(params: SignUpInput!): AuthResponse! signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse! login(params: LoginInput!): AuthResponse!
@@ -277,6 +282,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!

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)
} }

View File

@@ -293,7 +293,7 @@ func AuthorizeHandler() gin.HandlerFunc {
if authToken.RefreshToken != nil { if authToken.RefreshToken != nil {
res["refresh_token"] = authToken.RefreshToken.Token res["refresh_token"] = authToken.RefreshToken.Token
params += "&refresh_token=" + authToken.RefreshToken.Token params += "&refresh_token=" + authToken.RefreshToken.Token
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
} }
if isQuery { if isQuery {

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)
gc.JSON(http.StatusOK, gin.H{ if redirectURL != "" {
"message": "Logged out successfully", gc.Redirect(http.StatusFound, redirectURL)
}) } else {
gc.JSON(http.StatusOK, gin.H{
"message": "Logged out successfully",
})
}
} }
} }

View File

@@ -37,7 +37,7 @@ func OAuthCallbackHandler() gin.HandlerFunc {
} }
sessionstore.GetState(state) sessionstore.GetState(state)
// contains random token, redirect url, role // contains random token, redirect url, role
sessionSplit := strings.Split(state, "@") sessionSplit := strings.Split(state, "___")
if len(sessionSplit) < 3 { if len(sessionSplit) < 3 {
c.JSON(400, gin.H{"error": "invalid redirect url"}) c.JSON(400, gin.H{"error": "invalid redirect url"})
@@ -158,8 +158,8 @@ func OAuthCallbackHandler() gin.HandlerFunc {
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 {
params = params + `&refresh_token=${refresh_token}` params = params + `&refresh_token=` + authToken.RefreshToken.Token
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
} }
go utils.SaveSessionInDB(c, user.ID) go utils.SaveSessionInDB(c, user.ID)

View File

@@ -58,7 +58,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
roles = strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",") roles = strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",")
} }
oauthStateString := state + "@" + redirectURI + "@" + roles + "@" + strings.Join(scope, ",") oauthStateString := state + "___" + redirectURI + "___" + roles + "___" + strings.Join(scope, ",")
provider := c.Param("oauth_provider") provider := c.Param("oauth_provider")
isProviderConfigured := true isProviderConfigured := true

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,58 +64,95 @@ func TokenHandler() gin.HandlerFunc {
return return
} }
if codeVerifier == "" { var userID string
gc.JSON(http.StatusBadRequest, gin.H{ var roles, scope []string
"error": "invalid_code_verifier", if isAuthorizationCodeGrant {
"error_description": "The code verifier is required",
}) if codeVerifier == "" {
return gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code_verifier",
"error_description": "The code verifier is required",
})
return
}
if code == "" {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code",
"error_description": "The code is required",
})
return
}
hash := sha256.New()
hash.Write([]byte(codeVerifier))
encryptedCode := strings.ReplaceAll(base64.URLEncoding.EncodeToString(hash.Sum(nil)), "+", "-")
encryptedCode = strings.ReplaceAll(encryptedCode, "/", "_")
encryptedCode = strings.ReplaceAll(encryptedCode, "=", "")
sessionData := sessionstore.GetState(encryptedCode)
if sessionData == "" {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code_verifier",
"error_description": "The code verifier is invalid",
})
return
}
// split session data
// it contains code@sessiontoken
sessionDataSplit := strings.Split(sessionData, "@")
if sessionDataSplit[0] != code {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code_verifier",
"error_description": "The code verifier is invalid",
})
return
}
// rollover the session for security
sessionstore.RemoveState(sessionDataSplit[1])
// validate session
claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1])
if err != nil {
gc.JSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
"error_description": "Invalid session data",
})
return
}
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)
} }
if code == "" {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code",
"error_description": "The code is required",
})
return
}
hash := sha256.New()
hash.Write([]byte(codeVerifier))
encryptedCode := strings.ReplaceAll(base64.URLEncoding.EncodeToString(hash.Sum(nil)), "+", "-")
encryptedCode = strings.ReplaceAll(encryptedCode, "/", "_")
encryptedCode = strings.ReplaceAll(encryptedCode, "=", "")
sessionData := sessionstore.GetState(encryptedCode)
if sessionData == "" {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code_verifier",
"error_description": "The code verifier is invalid",
})
return
}
// split session data
// it contains code@sessiontoken
sessionDataSplit := strings.Split(sessionData, "@")
if sessionDataSplit[0] != code {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code_verifier",
"error_description": "The code verifier is invalid",
})
return
}
// validate session
claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1])
if err != nil {
gc.JSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
"error_description": "Invalid session data",
})
return
}
userID := claims.Subject
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

@@ -91,11 +91,11 @@ func VerifyEmailHandler() gin.HandlerFunc {
if authToken.RefreshToken != nil { if authToken.RefreshToken != nil {
params = params + `&refresh_token=${refresh_token}` params = params + `&refresh_token=${refresh_token}`
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
} }
if redirectURL == "" { if redirectURL == "" {
redirectURL = claim["redirect_url"].(string) redirectURL = claim["redirect_uri"].(string)
} }
if strings.Contains(redirectURL, "?") { if strings.Contains(redirectURL, "?") {

View File

@@ -84,7 +84,7 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
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

@@ -139,7 +139,7 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
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(),
@@ -147,8 +147,11 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
Nonce: nonceHash, Nonce: nonceHash,
RedirectURI: redirectURL, 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

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

@@ -80,7 +80,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

@@ -151,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

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

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

@@ -2,7 +2,6 @@ package token
import ( import (
"errors" "errors"
"fmt"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/crypto" "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") return claims, errors.New("invalid audience")
} }
fmt.Println("claims:", claims, claims["nonce"], nonce)
if claims["nonce"] != nonce { if claims["nonce"] != nonce {
return claims, errors.New("invalid nonce") return claims, errors.New("invalid nonce")
} }

View File

@@ -18,7 +18,7 @@ func CreateVerificationToken(email, tokenType, hostname, nonceHash, redirectURL
"iat": time.Now().Unix(), "iat": time.Now().Unix(),
"token_type": tokenType, "token_type": tokenType,
"nonce": nonceHash, "nonce": nonceHash,
"redirect_url": redirectURL, "redirect_uri": redirectURL,
} }
return SignJWTToken(claims) return SignJWTToken(claims)