├── dashboard ├── public │ ├── sample.csv │ └── roblox.png ├── favicon_io │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ └── android-chrome-512x512.png ├── .prettierrc.json ├── README.md ├── src │ ├── index.tsx │ ├── pages │ │ └── Home.tsx │ ├── layouts │ │ ├── DashboardLayout.tsx │ │ └── AuthLayout.tsx │ ├── components │ │ └── EnvComponents │ │ │ ├── DomainWhitelisting.tsx │ │ │ ├── SessionStorage.tsx │ │ │ └── OrganizationInfo.tsx │ ├── utils │ │ └── parseCSV.ts │ ├── App.tsx │ ├── contexts │ │ └── AuthContext.tsx │ └── routes │ │ └── index.tsx ├── esbuild.config.js └── package.json ├── .github ├── CODE_OF_CONDUCT.md ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── assets ├── logo.png └── authorizer-architecture.png ├── app ├── favicon_io │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ └── android-chrome-512x512.png ├── .prettierrc.json ├── src │ ├── index.tsx │ ├── pages │ │ ├── rest-password.tsx │ │ ├── setup-password.tsx │ │ ├── signup.tsx │ │ └── dashboard.tsx │ ├── theme.ts │ ├── index.css │ ├── utils │ │ └── common.ts │ └── App.tsx ├── README.md ├── esbuild.config.js └── package.json ├── docker-compose.yaml ├── server ├── constants │ ├── pagination.go │ ├── authenticator_method.go │ ├── cookie.go │ ├── token_types.go │ ├── webhook_event.go │ ├── oauth2.go │ ├── verification_types.go │ ├── db_types.go │ ├── auth_methods.go │ └── oauth_info_urls.go ├── tools.go ├── graph │ └── resolver.go ├── validators │ ├── email.go │ ├── common.go │ ├── roles.go │ ├── verification_requests.go │ ├── email_template.go │ ├── webhook.go │ ├── url.go │ └── password.go ├── refs │ ├── bool.go │ ├── int.go │ └── string.go ├── handlers │ ├── root.go │ ├── health.go │ ├── graphql.go │ ├── dashboard.go │ ├── jwks.go │ ├── playground.go │ ├── openid_config.go │ ├── userinfo.go │ └── logout.go ├── memorystore │ └── providers │ │ ├── inmemory │ │ ├── provider_test.go │ │ ├── provider.go │ │ └── stores │ │ │ ├── state_store.go │ │ │ └── env_store.go │ │ ├── redis │ │ └── provider_test.go │ │ └── providers.go ├── middlewares │ ├── context.go │ ├── client_check.go │ └── cors.go ├── db │ ├── providers │ │ ├── provider_template │ │ │ ├── provider.go │ │ │ ├── session.go │ │ │ ├── otp.go │ │ │ ├── webhook_log.go │ │ │ ├── env.go │ │ │ ├── authenticator.go │ │ │ ├── email_template.go │ │ │ ├── verification_requests.go │ │ │ └── webhook.go │ │ ├── dynamodb │ │ │ ├── session.go │ │ │ ├── shared.go │ │ │ ├── env.go │ │ │ └── authenticator.go │ │ ├── arangodb │ │ │ ├── session.go │ │ │ └── env.go │ │ ├── sql │ │ │ ├── session.go │ │ │ ├── env.go │ │ │ └── authenticator.go │ │ ├── couchbase │ │ │ ├── session.go │ │ │ ├── shared.go │ │ │ └── env.go │ │ ├── mongodb │ │ │ ├── session.go │ │ │ ├── env.go │ │ │ └── authenticator.go │ │ └── cassandradb │ │ │ ├── session.go │ │ │ └── env.go │ └── models │ │ ├── session.go │ │ ├── env.go │ │ ├── authenticators.go │ │ ├── otp.go │ │ ├── model.go │ │ ├── email_templates.go │ │ └── webhook_log.go ├── types │ └── interface_slice.go ├── authenticators │ ├── providers │ │ ├── totp │ │ │ └── provider.go │ │ └── providers.go │ └── totp_store.go ├── crypto │ ├── b64.go │ └── hmac.go ├── cli │ └── cli.go ├── utils │ ├── parser.go │ ├── response.go │ ├── generate_totp_recovery_code.go │ ├── request_info.go │ ├── gin_context.go │ ├── generate_otp.go │ ├── pagination.go │ ├── pkce.go │ ├── nonce.go │ └── file.go ├── resolvers │ ├── revoke.go │ ├── admin_logout.go │ ├── webhook.go │ ├── users.go │ ├── webhooks.go │ ├── profile.go │ ├── email_templates.go │ ├── verification_requests.go │ ├── logout.go │ ├── delete_webhook.go │ ├── admin_login.go │ ├── webhook_logs.go │ ├── delete_email_template.go │ ├── admin_session.go │ ├── user.go │ ├── enable_access.go │ ├── deactivate_account.go │ ├── revoke_access.go │ └── add_email_template.go ├── test │ ├── urls_test.go │ ├── meta_test.go │ ├── cors_test.go │ ├── admin_login_test.go │ ├── admin_signup_test.go │ ├── admin_logout_test.go │ ├── admin_session_test.go │ ├── env_test.go │ ├── email_templates_test.go │ ├── resend_verify_email_test.go │ ├── webhooks_test.go │ ├── env_file_test.go │ ├── test_endpoint_test.go │ ├── forgot_password_test.go │ ├── delete_user_test.go │ ├── delete_email_template_test.go │ ├── verify_email_test.go │ ├── users_test.go │ ├── webhook_test.go │ ├── deactivate_account_test.go │ ├── delete_webhook_test.go │ ├── profile_test.go │ ├── magic_link_login_test.go │ ├── verification_requests_test.go │ ├── invite_member_test.go │ ├── webhook_logs_test.go │ ├── update_profile_test.go │ └── add_webhook_test.go ├── token │ ├── verification_token.go │ └── admin_token.go ├── logs │ └── logs.go ├── smsproviders │ └── twilio.go └── cookie │ └── admin_cookie.go ├── .dockerignore ├── .env.sample ├── .gitignore ├── templates ├── authorize_web_message.tmpl ├── authorize_form_post.tmpl └── app.tmpl ├── .env.test ├── scripts ├── build-mac.sh └── couchbase-test.sh ├── LICENSE └── Dockerfile /dashboard/public/sample.csv: -------------------------------------------------------------------------------- 1 | foo@bar.com,test@authorizer.dev -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/assets/logo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: authorizerdev 4 | -------------------------------------------------------------------------------- /app/favicon_io/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/app/favicon_io/favicon.ico -------------------------------------------------------------------------------- /dashboard/public/roblox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/dashboard/public/roblox.png -------------------------------------------------------------------------------- /app/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /app/favicon_io/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/app/favicon_io/favicon-16x16.png -------------------------------------------------------------------------------- /app/favicon_io/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/app/favicon_io/favicon-32x32.png -------------------------------------------------------------------------------- /dashboard/favicon_io/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/dashboard/favicon_io/favicon.ico -------------------------------------------------------------------------------- /app/favicon_io/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/app/favicon_io/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/authorizer-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/assets/authorizer-architecture.png -------------------------------------------------------------------------------- /dashboard/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /dashboard/favicon_io/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/dashboard/favicon_io/favicon-16x16.png -------------------------------------------------------------------------------- /dashboard/favicon_io/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/dashboard/favicon_io/favicon-32x32.png -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | authorizer: 3 | build: . 4 | ports: 5 | - 8080:8080 6 | env_file: 7 | - .env 8 | -------------------------------------------------------------------------------- /server/constants/pagination.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // DefaultLimit is the default limit for pagination 4 | var DefaultLimit = 10 5 | -------------------------------------------------------------------------------- /server/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "github.com/99designs/gqlgen" 8 | ) 9 | -------------------------------------------------------------------------------- /app/favicon_io/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/app/favicon_io/android-chrome-192x192.png -------------------------------------------------------------------------------- /app/favicon_io/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/app/favicon_io/android-chrome-512x512.png -------------------------------------------------------------------------------- /dashboard/favicon_io/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/dashboard/favicon_io/apple-touch-icon.png -------------------------------------------------------------------------------- /dashboard/favicon_io/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/dashboard/favicon_io/android-chrome-192x192.png -------------------------------------------------------------------------------- /dashboard/favicon_io/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/authorizer/public-package/dashboard/favicon_io/android-chrome-512x512.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | server/.env 2 | server/server 3 | .git 4 | .gitignore 5 | README.md 6 | ROADMAP.md 7 | build 8 | .env 9 | data.db 10 | app/node_modules 11 | app/build 12 | certs/ 13 | -------------------------------------------------------------------------------- /app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /server/constants/authenticator_method.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // Authenticators Methods 4 | const ( 5 | // EnvKeyTOTPAuthenticator key for env variable TOTP 6 | EnvKeyTOTPAuthenticator = "totp" 7 | ) 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### What does this PR do? 2 | 3 | #### Which issue(s) does this PR fix? 4 | 5 | #### If this PR affects any API reference documentation, please share the updated endpoint references 6 | -------------------------------------------------------------------------------- /dashboard/README.md: -------------------------------------------------------------------------------- 1 | # Authorizer dashboard 2 | 3 | ### Getting started 4 | 5 | **Setting up locally** 6 | 7 | - `cd dashboard` 8 | - `npm start` 9 | 10 | **Creating production build** 11 | 12 | - `make build-dashboard` 13 | -------------------------------------------------------------------------------- /server/graph/resolver.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | // This file will not be regenerated automatically. 4 | // 5 | // It serves as dependency injection for your app, add any dependencies you require here. 6 | 7 | type Resolver struct{} 8 | -------------------------------------------------------------------------------- /dashboard/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 |
7 | 8 |
, 9 | document.getElementById('root'), 10 | ); 11 | -------------------------------------------------------------------------------- /server/validators/email.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import "net/mail" 4 | 5 | // IsValidEmail validates email 6 | func IsValidEmail(email string) bool { 7 | _, err := mail.ParseAddress(email) 8 | return err == nil 9 | } 10 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | ENV=production 2 | DATABASE_URL=data.db 3 | DATABASE_TYPE=sqlite 4 | CUSTOM_ACCESS_TOKEN_SCRIPT="function(user,tokenPayload){var data = tokenPayload;data.extra = {'x-extra-id': user.id};return data;}" 5 | DISABLE_PLAYGROUND=true 6 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # Authorizer APP 2 | 3 | App that can be used as login wall for your any application in combination with @authorizerdev/@authorizer.js 4 | 5 | ### Getting started 6 | 7 | **Setting up locally** 8 | 9 | - `cd app` 10 | - `npm start` 11 | 12 | **Creating production build** 13 | 14 | - `make build-app` 15 | -------------------------------------------------------------------------------- /server/refs/bool.go: -------------------------------------------------------------------------------- 1 | package refs 2 | 3 | // NewBoolRef returns a reference to a bool with given value 4 | func NewBoolRef(v bool) *bool { 5 | return &v 6 | } 7 | 8 | // BoolValue returns the value of the given bool ref 9 | func BoolValue(r *bool) bool { 10 | if r == nil { 11 | return false 12 | } 13 | return *r 14 | } -------------------------------------------------------------------------------- /app/esbuild.config.js: -------------------------------------------------------------------------------- 1 | const __is_prod__ = process.env.NODE_ENV === 'production'; 2 | require('esbuild').build({ 3 | entryPoints: ['src/index.tsx'], 4 | chunkNames: '[name]-[hash]', 5 | bundle: true, 6 | minify: __is_prod__, 7 | outdir: 'build', 8 | splitting: true, 9 | format: 'esm', 10 | watch: !__is_prod__, 11 | }); 12 | -------------------------------------------------------------------------------- /server/handlers/root.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // RootHandler is the handler for / root route. 10 | func RootHandler() gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | c.Redirect(http.StatusTemporaryRedirect, "/dashboard") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/refs/int.go: -------------------------------------------------------------------------------- 1 | package refs 2 | 3 | // NewInt64Ref returns a reference to a int64 with given value 4 | func NewInt64Ref(v int64) *int64 { 5 | return &v 6 | } 7 | 8 | // Int64Value returns the value of the given bool ref 9 | func Int64Value(r *int64) int64 { 10 | if r == nil { 11 | return 0 12 | } 13 | return *r 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | server/server 2 | server/.env 3 | data 4 | app/node_modules 5 | app/build 6 | dashboard/node_modules 7 | dashboard/build 8 | build 9 | .env 10 | data.db 11 | test.db 12 | .DS_Store 13 | .env.local 14 | *.tar.gz 15 | .vscode/ 16 | .yalc 17 | yalc.lock 18 | certs/ 19 | *-shm 20 | *-wal 21 | .idea 22 | *.iml 23 | *.code-workspace 24 | tmp/ -------------------------------------------------------------------------------- /dashboard/esbuild.config.js: -------------------------------------------------------------------------------- 1 | const __is_prod__ = process.env.NODE_ENV === 'production'; 2 | require('esbuild').build({ 3 | entryPoints: ['src/index.tsx'], 4 | chunkNames: '[name]-[hash]', 5 | bundle: true, 6 | minify: __is_prod__, 7 | outdir: 'build', 8 | splitting: true, 9 | format: 'esm', 10 | watch: !__is_prod__, 11 | logLevel: 'info', 12 | }); 13 | -------------------------------------------------------------------------------- /server/handlers/health.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // HealthHandler is the handler for /health route. 10 | // It states if server is in healthy state or not 11 | func HealthHandler() gin.HandlerFunc { 12 | return func(c *gin.Context) { 13 | c.String(http.StatusOK, "OK") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/validators/common.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | // IsStringArrayEqual validates if string array are equal. 4 | // This does check if the order is same 5 | func IsStringArrayEqual(a, b []string) bool { 6 | if len(a) != len(b) { 7 | return false 8 | } 9 | for i, v := range a { 10 | if v != b[i] { 11 | return false 12 | } 13 | } 14 | return true 15 | } 16 | -------------------------------------------------------------------------------- /app/src/pages/rest-password.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { AuthorizerResetPassword } from '@authorizerdev/authorizer-react'; 3 | 4 | export default function ResetPassword() { 5 | return ( 6 | 7 |

Reset Password

8 |
9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/pages/setup-password.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { AuthorizerResetPassword } from '@authorizerdev/authorizer-react'; 3 | 4 | export default function SetupPassword() { 5 | return ( 6 | 7 |

Setup new Password

8 |
9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /server/memorystore/providers/inmemory/provider_test.go: -------------------------------------------------------------------------------- 1 | package inmemory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/authorizerdev/authorizer/server/memorystore/providers" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestInMemoryProvider(t *testing.T) { 11 | p, err := NewInMemoryProvider() 12 | assert.NoError(t, err) 13 | providers.ProviderTests(t, p) 14 | } 15 | -------------------------------------------------------------------------------- /server/memorystore/providers/redis/provider_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/authorizerdev/authorizer/server/memorystore/providers" 9 | ) 10 | 11 | func TestRedisProvider(t *testing.T) { 12 | p, err := NewRedisProvider("redis://127.0.0.1:6379") 13 | assert.NoError(t, err) 14 | providers.ProviderTests(t, p) 15 | } 16 | -------------------------------------------------------------------------------- /server/validators/roles.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import "github.com/authorizerdev/authorizer/server/utils" 4 | 5 | // IsValidRoles validates roles 6 | func IsValidRoles(userRoles []string, roles []string) bool { 7 | valid := true 8 | for _, userRole := range userRoles { 9 | if !utils.StringSliceContains(roles, userRole) { 10 | valid = false 11 | break 12 | } 13 | } 14 | 15 | return valid 16 | } 17 | -------------------------------------------------------------------------------- /server/constants/cookie.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // AppCookieName is the name of the cookie that is used to store the application token 5 | AppCookieName = "cookie" 6 | // AdminCookieName is the name of the cookie that is used to store the admin token 7 | AdminCookieName = "authorizer-admin" 8 | // MfaCookieName is the name of the cookie that is used to store the mfa session 9 | MfaCookieName = "mfa" 10 | ) 11 | -------------------------------------------------------------------------------- /server/refs/string.go: -------------------------------------------------------------------------------- 1 | package refs 2 | 3 | // NewStringRef returns a reference to a string with given value 4 | func NewStringRef(v string) *string { 5 | return &v 6 | } 7 | 8 | // StringValue returns the value of the given string ref 9 | func StringValue(r *string, defaultValue ...string) string { 10 | if r != nil { 11 | return *r 12 | } 13 | if len(defaultValue) > 0 { 14 | return defaultValue[0] 15 | } 16 | return "" 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /server/middlewares/context.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // GinContextToContextMiddleware is a middleware to add gin context in context 10 | func GinContextToContextMiddleware() gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | ctx := context.WithValue(c.Request.Context(), "GinContextKey", c) 13 | c.Request = c.Request.WithContext(ctx) 14 | c.Next() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/db/providers/provider_template/provider.go: -------------------------------------------------------------------------------- 1 | package provider_template 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | // TODO change following provider to new db provider 8 | type provider struct { 9 | db *gorm.DB 10 | } 11 | 12 | // NewProvider returns a new SQL provider 13 | // TODO change following provider to new db provider 14 | func NewProvider() (*provider, error) { 15 | var sqlDB *gorm.DB 16 | 17 | return &provider{ 18 | db: sqlDB, 19 | }, nil 20 | } 21 | -------------------------------------------------------------------------------- /server/types/interface_slice.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "encoding/json" 4 | 5 | // Type for interface slice. Used for redis store. 6 | type InterfaceSlice []interface{} 7 | 8 | // MarshalBinary for interface slice. 9 | func (s InterfaceSlice) MarshalBinary() ([]byte, error) { 10 | return json.Marshal(s) 11 | } 12 | 13 | // UnmarshalBinary for interface slice. 14 | func (s *InterfaceSlice) UnmarshalBinary(data []byte) error { 15 | return json.Unmarshal(data, s) 16 | } 17 | -------------------------------------------------------------------------------- /templates/authorize_web_message.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Authorization Response 5 | 6 | 7 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /server/authenticators/providers/totp/provider.go: -------------------------------------------------------------------------------- 1 | package totp 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type provider struct { 8 | ctx context.Context 9 | } 10 | 11 | // TOTPConfig defines totp config 12 | type TOTPConfig struct { 13 | ScannerImage string 14 | Secret string 15 | } 16 | 17 | // NewProvider returns a new totp provider 18 | func NewProvider() (*provider, error) { 19 | ctx := context.Background() 20 | return &provider{ 21 | ctx: ctx, 22 | }, nil 23 | } 24 | -------------------------------------------------------------------------------- /server/crypto/b64.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import "encoding/base64" 4 | 5 | // EncryptB64 encrypts data into base64 string 6 | func EncryptB64(text string) string { 7 | return base64.StdEncoding.EncodeToString([]byte(text)) 8 | } 9 | 10 | // DecryptB64 decrypts from base64 string to readable string 11 | func DecryptB64(s string) (string, error) { 12 | data, err := base64.StdEncoding.DecodeString(s) 13 | if err != nil { 14 | return "", err 15 | } 16 | return string(data), nil 17 | } 18 | -------------------------------------------------------------------------------- /server/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | var ( 4 | // ARG_DB_URL is the cli arg variable for the database url 5 | ARG_DB_URL *string 6 | // ARG_DB_TYPE is the cli arg variable for the database type 7 | ARG_DB_TYPE *string 8 | // ARG_ENV_FILE is the cli arg variable for the env file 9 | ARG_ENV_FILE *string 10 | // ARG_LOG_LEVEL is the cli arg variable for the log level 11 | ARG_LOG_LEVEL *string 12 | // ARG_REDIS_URL is the cli arg variable for the redis url 13 | ARG_REDIS_URL *string 14 | ) 15 | -------------------------------------------------------------------------------- /server/utils/parser.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | // ParseDurationInSeconds parses input s, removes ms/us/ns and returns result duration 9 | func ParseDurationInSeconds(s string) (time.Duration, error) { 10 | d, err := time.ParseDuration(s) 11 | if err != nil { 12 | return 0, err 13 | } 14 | 15 | d = d.Truncate(time.Second) 16 | if d <= 0 { 17 | return 0, errors.New(`duration must be greater than 0s`) 18 | } 19 | 20 | return d, nil 21 | } 22 | -------------------------------------------------------------------------------- /server/constants/token_types.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // TokenTypeRefreshToken is the refresh_token token type 5 | TokenTypeRefreshToken = "refresh_token" 6 | // TokenTypeAccessToken is the access_token token type 7 | TokenTypeAccessToken = "access_token" 8 | // TokenTypeIdentityToken is the identity_token token type 9 | TokenTypeIdentityToken = "id_token" 10 | // TokenTypeSessionToken is the session_token type used for browser session 11 | TokenTypeSessionToken = "session_token" 12 | ) 13 | -------------------------------------------------------------------------------- /server/utils/response.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // HandleRedirectORJsonResponse handles the response based on redirectURL 11 | func HandleRedirectORJsonResponse(c *gin.Context, httpResponse int, response map[string]interface{}, redirectURL string) { 12 | if strings.TrimSpace(redirectURL) == "" { 13 | c.JSON(httpResponse, response) 14 | } else { 15 | c.Redirect(http.StatusTemporaryRedirect, redirectURL) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /dashboard/src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from '@chakra-ui/react'; 2 | import React from 'react'; 3 | 4 | export default function Home() { 5 | return ( 6 | <> 7 | 8 | Hi there 👋
9 |
10 | 11 | 12 | Welcome to Authorizer Administrative Dashboard!
13 | Please use this dashboard to configure your environment variables or 14 | have look at your users 15 |
16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /server/resolvers/revoke.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/authorizerdev/authorizer/server/graph/model" 7 | "github.com/authorizerdev/authorizer/server/memorystore" 8 | ) 9 | 10 | // RevokeResolver resolver to revoke refresh token 11 | func RevokeResolver(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) { 12 | memorystore.Provider.RemoveState(params.RefreshToken) 13 | return &model.Response{ 14 | Message: "Token revoked", 15 | }, nil 16 | } 17 | -------------------------------------------------------------------------------- /server/utils/generate_totp_recovery_code.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | // GenerateTOTPRecoveryCode generates a random 16-character recovery code. 9 | func GenerateTOTPRecoveryCode() string { 10 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 11 | code := make([]byte, 16) 12 | 13 | rand.Seed(time.Now().UnixNano()) 14 | for i := range code { 15 | code[i] = charset[rand.Intn(len(charset))] 16 | } 17 | 18 | return string(code) 19 | } 20 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | ENV=test 2 | DATABASE_URL=test.db 3 | DATABASE_TYPE=sqlite 4 | CUSTOM_ACCESS_TOKEN_SCRIPT="function(user,tokenPayload){var data = tokenPayload;data.extra = {'x-extra-id': user.id};return data;}" 5 | SMTP_HOST=smtp.mailtrap.io 6 | SMTP_PORT=2525 7 | SMTP_USERNAME=test 8 | SMTP_PASSWORD=test 9 | SENDER_EMAIL="info@authorizer.dev" 10 | TWILIO_API_KEY=test 11 | TWILIO_API_SECRET=test 12 | TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 13 | TWILIO_SENDER=909921212112 14 | SENDER_NAME="Authorizer" 15 | AWS_REGION=ap-south-1 -------------------------------------------------------------------------------- /templates/authorize_form_post.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Authorization Response 5 | 6 | 7 |
8 | {{ range $key, $val := .authorization_response }} 9 | 10 | {{ end }} 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /server/utils/request_info.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "net/http" 4 | 5 | // GetIP helps in getting the IP address from the request 6 | func GetIP(r *http.Request) string { 7 | IPAddress := r.Header.Get("X-Real-Ip") 8 | if IPAddress == "" { 9 | IPAddress = r.Header.Get("X-Forwarded-For") 10 | } 11 | 12 | if IPAddress == "" { 13 | IPAddress = r.RemoteAddr 14 | } 15 | return IPAddress 16 | } 17 | 18 | // GetUserAgent helps in getting the user agent from the request 19 | func GetUserAgent(r *http.Request) string { 20 | return r.UserAgent() 21 | } 22 | -------------------------------------------------------------------------------- /server/validators/verification_requests.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import "github.com/authorizerdev/authorizer/server/constants" 4 | 5 | // IsValidVerificationIdentifier validates verification identifier that is used to identify 6 | // the type of verification request 7 | func IsValidVerificationIdentifier(identifier string) bool { 8 | if identifier != constants.VerificationTypeBasicAuthSignup && identifier != constants.VerificationTypeForgotPassword && identifier != constants.VerificationTypeUpdateEmail { 9 | return false 10 | } 11 | return true 12 | } 13 | -------------------------------------------------------------------------------- /server/utils/gin_context.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // GinContext to get gin context from context 11 | func GinContextFromContext(ctx context.Context) (*gin.Context, error) { 12 | ginContext := ctx.Value("GinContextKey") 13 | if ginContext == nil { 14 | err := fmt.Errorf("could not retrieve gin.Context") 15 | return nil, err 16 | } 17 | 18 | gc, ok := ginContext.(*gin.Context) 19 | if !ok { 20 | err := fmt.Errorf("gin.Context has wrong type") 21 | return nil, err 22 | } 23 | return gc, nil 24 | } 25 | -------------------------------------------------------------------------------- /app/src/theme.ts: -------------------------------------------------------------------------------- 1 | // colors: https://tailwindcss.com/docs/customizing-colors 2 | 3 | export const theme = { 4 | colors: { 5 | primary: '#3B82F6', 6 | primaryDisabled: '#60A5FA', 7 | gray: '#D1D5DB', 8 | danger: '#DC2626', 9 | success: '#10B981', 10 | textColor: '#374151', 11 | }, 12 | fonts: { 13 | // typography 14 | fontStack: '-apple-system, system-ui, sans-serif', 15 | 16 | // font sizes 17 | largeText: '18px', 18 | mediumText: '14px', 19 | smallText: '12px', 20 | tinyText: '10px', 21 | }, 22 | 23 | radius: { 24 | card: '5px', 25 | button: '5px', 26 | input: '5px', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /server/utils/generate_otp.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | // GenerateOTP to generate random 6 digit otp 9 | func GenerateOTP() string { 10 | code := "" 11 | codeLength := 6 12 | charSet := "ABCDEFGHJKLMNPQRSTUVWXYZ123456789" 13 | charSetLength := int32(len(charSet)) 14 | for i := 0; i < codeLength; i++ { 15 | index := randomNumber(0, charSetLength) 16 | code += string(charSet[index]) 17 | } 18 | 19 | return code 20 | } 21 | 22 | func randomNumber(min, max int32) int32 { 23 | rand.Seed(time.Now().UnixNano()) 24 | return min + int32(rand.Intn(int(max-min))) 25 | } 26 | -------------------------------------------------------------------------------- /server/validators/email_template.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import "github.com/authorizerdev/authorizer/server/constants" 4 | 5 | // IsValidEmailTemplateEventName function to validate email template events 6 | func IsValidEmailTemplateEventName(eventName string) bool { 7 | if eventName != constants.VerificationTypeBasicAuthSignup && eventName != constants.VerificationTypeForgotPassword && eventName != constants.VerificationTypeMagicLinkLogin && eventName != constants.VerificationTypeUpdateEmail && eventName != constants.VerificationTypeOTP && eventName != constants.VerificationTypeInviteMember { 8 | return false 9 | } 10 | 11 | return true 12 | } 13 | -------------------------------------------------------------------------------- /server/validators/webhook.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import "github.com/authorizerdev/authorizer/server/constants" 4 | 5 | // IsValidWebhookEventName to validate webhook event name 6 | func IsValidWebhookEventName(eventName string) bool { 7 | if eventName != constants.UserCreatedWebhookEvent && eventName != constants.UserLoginWebhookEvent && eventName != constants.UserSignUpWebhookEvent && eventName != constants.UserDeletedWebhookEvent && eventName != constants.UserAccessEnabledWebhookEvent && eventName != constants.UserAccessRevokedWebhookEvent && eventName != constants.UserDeactivatedWebhookEvent { 8 | return false 9 | } 10 | 11 | return true 12 | } 13 | -------------------------------------------------------------------------------- /app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 10; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | color: #374151; 9 | font-size: 14px; 10 | } 11 | 12 | *, 13 | *:before, 14 | *:after { 15 | box-sizing: inherit; 16 | } 17 | 18 | .container { 19 | box-sizing: content-box; 20 | border: 1px solid #d1d5db; 21 | padding: 25px 20px; 22 | border-radius: 5px; 23 | } 24 | 25 | @media only screen and (min-width: 768px) { 26 | .container { 27 | width: 400px; 28 | margin: 0 auto; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/crypto/hmac.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | // NewHMAC key returns new key that can be used to ecnrypt data using HMAC algo 8 | // returns key, string, error 9 | func NewHMACKey(algo, keyID string) (string, string, error) { 10 | key := uuid.New().String() 11 | jwkPublicKey, err := GetPubJWK(algo, keyID, []byte(key)) 12 | if err != nil { 13 | return "", "", err 14 | } 15 | return key, string(jwkPublicKey), nil 16 | } 17 | 18 | // IsHMACValid checks if given string is valid HMCA algo 19 | func IsHMACA(algo string) bool { 20 | switch algo { 21 | case "HS256", "HS384", "HS512": 22 | return true 23 | default: 24 | return false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/handlers/graphql.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/99designs/gqlgen/graphql/handler" 5 | "github.com/authorizerdev/authorizer/server/graph" 6 | "github.com/authorizerdev/authorizer/server/graph/generated" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // GraphqlHandler is the main handler that handels all the graphql requests 11 | func GraphqlHandler() gin.HandlerFunc { 12 | // NewExecutableSchema and Config are in the generated.go file 13 | // Resolver is in the resolver.go file 14 | h := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) 15 | 16 | return func(c *gin.Context) { 17 | h.ServeHTTP(c.Writer, c.Request) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea or enhancement for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | **Feature Description** 10 | 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | 17 | **Describe alternatives you've considered** 18 | 19 | 20 | 21 | **Additional context** 22 | 23 | 24 | -------------------------------------------------------------------------------- /server/db/providers/provider_template/session.go: -------------------------------------------------------------------------------- 1 | package provider_template 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/authorizerdev/authorizer/server/db/models" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // AddSession to save session information in database 12 | func (p *provider) AddSession(ctx context.Context, session *models.Session) error { 13 | if session.ID == "" { 14 | session.ID = uuid.New().String() 15 | } 16 | session.CreatedAt = time.Now().Unix() 17 | session.UpdatedAt = time.Now().Unix() 18 | return nil 19 | } 20 | 21 | // DeleteSession to delete session information from database 22 | func (p *provider) DeleteSession(ctx context.Context, userId string) error { 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /server/test/urls_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/authorizerdev/authorizer/server/parsers" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetHostName(t *testing.T) { 11 | url := "http://test.herokuapp.com:80" 12 | 13 | host, port := parsers.GetHostParts(url) 14 | expectedHost := "test.herokuapp.com" 15 | 16 | assert.Equal(t, host, expectedHost, "hostname should be equal") 17 | assert.Equal(t, port, "80", "port should be 80") 18 | } 19 | 20 | func TestGetDomainName(t *testing.T) { 21 | url := "http://test.herokuapp.com" 22 | 23 | got := parsers.GetDomainName(url) 24 | want := "herokuapp.com" 25 | 26 | assert.Equal(t, got, want, "domain name should be equal") 27 | } 28 | -------------------------------------------------------------------------------- /server/test/meta_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/resolvers" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func metaTests(t *testing.T, s TestSetup) { 12 | t.Helper() 13 | t.Run(`should get meta information`, func(t *testing.T) { 14 | ctx := context.Background() 15 | meta, err := resolvers.MetaResolver(ctx) 16 | assert.Nil(t, err) 17 | assert.False(t, meta.IsFacebookLoginEnabled) 18 | assert.False(t, meta.IsGoogleLoginEnabled) 19 | assert.False(t, meta.IsGithubLoginEnabled) 20 | assert.True(t, meta.IsEmailVerificationEnabled) 21 | assert.True(t, meta.IsBasicAuthenticationEnabled) 22 | assert.True(t, meta.IsMagicLinkLoginEnabled) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /server/handlers/dashboard.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/authorizerdev/authorizer/server/constants" 7 | "github.com/authorizerdev/authorizer/server/memorystore" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // DashboardHandler is the handler for the /dashboard route 12 | func DashboardHandler() gin.HandlerFunc { 13 | return func(c *gin.Context) { 14 | isOnboardingCompleted := false 15 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 16 | if err != nil || adminSecret != "" { 17 | isOnboardingCompleted = true 18 | } 19 | 20 | c.HTML(http.StatusOK, "dashboard.tmpl", gin.H{ 21 | "data": map[string]interface{}{ 22 | "isOnboardingCompleted": isOnboardingCompleted, 23 | }, 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/memorystore/providers/inmemory/provider.go: -------------------------------------------------------------------------------- 1 | package inmemory 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/authorizerdev/authorizer/server/memorystore/providers/inmemory/stores" 7 | ) 8 | 9 | type provider struct { 10 | mutex sync.Mutex 11 | sessionStore *stores.SessionStore 12 | mfasessionStore *stores.SessionStore 13 | stateStore *stores.StateStore 14 | envStore *stores.EnvStore 15 | } 16 | 17 | // NewInMemoryStore returns a new in-memory store. 18 | func NewInMemoryProvider() (*provider, error) { 19 | return &provider{ 20 | mutex: sync.Mutex{}, 21 | envStore: stores.NewEnvStore(), 22 | sessionStore: stores.NewSessionStore(), 23 | mfasessionStore: stores.NewSessionStore(), 24 | stateStore: stores.NewStateStore(), 25 | }, nil 26 | } 27 | -------------------------------------------------------------------------------- /server/db/providers/dynamodb/session.go: -------------------------------------------------------------------------------- 1 | package dynamodb 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/authorizerdev/authorizer/server/db/models" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // AddSession to save session information in database 12 | func (p *provider) AddSession(ctx context.Context, session *models.Session) error { 13 | collection := p.db.Table(models.Collections.Session) 14 | if session.ID == "" { 15 | session.ID = uuid.New().String() 16 | } 17 | session.CreatedAt = time.Now().Unix() 18 | session.UpdatedAt = time.Now().Unix() 19 | err := collection.Put(session).RunWithContext(ctx) 20 | return err 21 | } 22 | 23 | // DeleteSession to delete session information from database 24 | func (p *provider) DeleteSession(ctx context.Context, userId string) error { 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /server/test/cors_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCors(t *testing.T) { 11 | allowedOrigin := "http://localhost:8080" // The allowed origin that you want to check 12 | notAllowedOrigin := "http://myapp.com" 13 | 14 | s := testSetup() 15 | defer s.Server.Close() 16 | client := &http.Client{} 17 | 18 | req, _ := createContext(s) 19 | req.Header.Add("Origin", allowedOrigin) 20 | res, _ := client.Do(req) 21 | 22 | // You should get your origin (or a * depending on your config) if the 23 | // passed origin is allowed. 24 | o := res.Header.Get("Access-Control-Allow-Origin") 25 | assert.NotEqual(t, o, notAllowedOrigin, "Origins should not match") 26 | assert.Equal(t, o, allowedOrigin, "Origins do match") 27 | } 28 | -------------------------------------------------------------------------------- /app/src/utils/common.ts: -------------------------------------------------------------------------------- 1 | export const getCrypto = () => { 2 | //ie 11.x uses msCrypto 3 | return (window.crypto || (window as any).msCrypto) as Crypto; 4 | }; 5 | 6 | export const createRandomString = () => { 7 | const charset = 8 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_~.'; 9 | let random = ''; 10 | const randomValues = Array.from( 11 | getCrypto().getRandomValues(new Uint8Array(43)), 12 | ); 13 | randomValues.forEach((v) => (random += charset[v % charset.length])); 14 | return random; 15 | }; 16 | 17 | export const createQueryParams = (params: any) => { 18 | return Object.keys(params) 19 | .filter((k) => typeof params[k] !== 'undefined') 20 | .map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) 21 | .join('&'); 22 | }; 23 | 24 | export const hasWindow = (): boolean => typeof window !== 'undefined'; 25 | -------------------------------------------------------------------------------- /app/src/pages/signup.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { AuthorizerSignup, AuthorizerSocialLogin } from '@authorizerdev/authorizer-react'; 3 | import styled from 'styled-components'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | const FooterContent = styled.div` 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | margin-top: 20px; 11 | `; 12 | 13 | export default function SignUp({ 14 | urlProps, 15 | }: { 16 | urlProps: Record; 17 | }) { 18 | return ( 19 | 20 |

Sign Up

21 |
22 | 23 | 24 | 25 | Already have an account? Login 26 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /server/db/providers/provider_template/otp.go: -------------------------------------------------------------------------------- 1 | package provider_template 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/authorizerdev/authorizer/server/db/models" 7 | ) 8 | 9 | // UpsertOTP to add or update otp 10 | func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { 11 | return nil, nil 12 | } 13 | 14 | // GetOTPByEmail to get otp for a given email address 15 | func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { 16 | return nil, nil 17 | } 18 | 19 | // GetOTPByPhoneNumber to get otp for a given phone number 20 | func (p *provider) GetOTPByPhoneNumber(ctx context.Context, phoneNumber string) (*models.OTP, error) { 21 | return nil, nil 22 | } 23 | 24 | // DeleteOTP to delete otp 25 | func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error { 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /templates/app.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.data.organizationName}} 6 | 7 | 8 | 9 | 10 | 11 | Document 12 | 13 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /server/utils/pagination.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/authorizerdev/authorizer/server/constants" 5 | "github.com/authorizerdev/authorizer/server/graph/model" 6 | ) 7 | 8 | // GetPagination helps getting pagination data from paginated input 9 | // also returns default limit and offset if pagination data is not present 10 | func GetPagination(paginatedInput *model.PaginatedInput) *model.Pagination { 11 | limit := int64(constants.DefaultLimit) 12 | page := int64(1) 13 | if paginatedInput != nil && paginatedInput.Pagination != nil { 14 | if paginatedInput.Pagination.Limit != nil { 15 | limit = *paginatedInput.Pagination.Limit 16 | } 17 | 18 | if paginatedInput.Pagination.Page != nil { 19 | page = *paginatedInput.Pagination.Page 20 | } 21 | } 22 | 23 | return &model.Pagination{ 24 | Limit: limit, 25 | Offset: (page - 1) * limit, 26 | Page: page, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/utils/pkce.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/sha256" 5 | b64 "encoding/base64" 6 | "math/rand" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | const ( 12 | length = 32 13 | ) 14 | 15 | // GenerateCodeChallenge creates PKCE-Code-Challenge 16 | // and returns the verifier and challenge 17 | func GenerateCodeChallenge() (string, string) { 18 | // Generate Verifier 19 | randGenerator := rand.New(rand.NewSource(time.Now().UnixNano())) 20 | randomBytes := make([]byte, length) 21 | for i := 0; i < length; i++ { 22 | randomBytes[i] = byte(randGenerator.Intn(255)) 23 | } 24 | verifier := strings.Trim(b64.URLEncoding.EncodeToString(randomBytes), "=") 25 | 26 | // Generate Challenge 27 | rawChallenge := sha256.New() 28 | rawChallenge.Write([]byte(verifier)) 29 | challenge := strings.Trim(b64.URLEncoding.EncodeToString(rawChallenge.Sum(nil)), "=") 30 | 31 | return verifier, challenge 32 | } 33 | -------------------------------------------------------------------------------- /server/db/providers/arangodb/session.go: -------------------------------------------------------------------------------- 1 | package arangodb 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/authorizerdev/authorizer/server/db/models" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // AddSession to save session information in database 12 | func (p *provider) AddSession(ctx context.Context, session *models.Session) error { 13 | if session.ID == "" { 14 | session.ID = uuid.New().String() 15 | session.Key = session.ID 16 | } 17 | session.CreatedAt = time.Now().Unix() 18 | session.UpdatedAt = time.Now().Unix() 19 | sessionCollection, _ := p.db.Collection(ctx, models.Collections.Session) 20 | _, err := sessionCollection.CreateDocument(ctx, session) 21 | if err != nil { 22 | return err 23 | } 24 | return nil 25 | } 26 | 27 | // DeleteSession to delete session information from database 28 | func (p *provider) DeleteSession(ctx context.Context, userId string) error { 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /server/db/providers/sql/session.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/authorizerdev/authorizer/server/db/models" 8 | "github.com/google/uuid" 9 | "gorm.io/gorm/clause" 10 | ) 11 | 12 | // AddSession to save session information in database 13 | func (p *provider) AddSession(ctx context.Context, session *models.Session) error { 14 | if session.ID == "" { 15 | session.ID = uuid.New().String() 16 | } 17 | 18 | session.Key = session.ID 19 | session.CreatedAt = time.Now().Unix() 20 | session.UpdatedAt = time.Now().Unix() 21 | res := p.db.Clauses( 22 | clause.OnConflict{ 23 | DoNothing: true, 24 | }).Create(&session) 25 | if res.Error != nil { 26 | return res.Error 27 | } 28 | return nil 29 | } 30 | 31 | // DeleteSession to delete session information from database 32 | func (p *provider) DeleteSession(ctx context.Context, userId string) error { 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /server/db/providers/provider_template/webhook_log.go: -------------------------------------------------------------------------------- 1 | package provider_template 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/authorizerdev/authorizer/server/db/models" 8 | "github.com/authorizerdev/authorizer/server/graph/model" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | // AddWebhookLog to add webhook log 13 | func (p *provider) AddWebhookLog(ctx context.Context, webhookLog *models.WebhookLog) (*model.WebhookLog, error) { 14 | if webhookLog.ID == "" { 15 | webhookLog.ID = uuid.New().String() 16 | } 17 | 18 | webhookLog.Key = webhookLog.ID 19 | webhookLog.CreatedAt = time.Now().Unix() 20 | webhookLog.UpdatedAt = time.Now().Unix() 21 | return webhookLog.AsAPIWebhookLog(), nil 22 | } 23 | 24 | // ListWebhookLogs to list webhook logs 25 | func (p *provider) ListWebhookLogs(ctx context.Context, pagination *model.Pagination, webhookID string) (*model.WebhookLogs, error) { 26 | return nil, nil 27 | } 28 | -------------------------------------------------------------------------------- /server/constants/webhook_event.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | 5 | // UserLoginWebhookEvent name for login event 6 | UserLoginWebhookEvent = `user.login` 7 | // UserCreatedWebhookEvent name for user creation event 8 | // This is triggered when user entry is created but still not verified 9 | UserCreatedWebhookEvent = `user.created` 10 | // UserSignUpWebhookEvent name for signup event 11 | UserSignUpWebhookEvent = `user.signup` 12 | // UserAccessRevokedWebhookEvent name for user access revoke event 13 | UserAccessRevokedWebhookEvent = `user.access_revoked` 14 | // UserAccessEnabledWebhookEvent name for user access enable event 15 | UserAccessEnabledWebhookEvent = `user.access_enabled` 16 | // UserDeletedWebhookEvent name for user deleted event 17 | UserDeletedWebhookEvent = `user.deleted` 18 | // UserDeactivatedWebhookEvent name for user deactivated event 19 | UserDeactivatedWebhookEvent = `user.deactivated` 20 | ) 21 | -------------------------------------------------------------------------------- /server/utils/nonce.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | 6 | "github.com/authorizerdev/authorizer/server/crypto" 7 | ) 8 | 9 | // GenerateNonce generats random nonce string and returns 10 | // the nonce string, nonce hash, error 11 | func GenerateNonce() (string, string, error) { 12 | nonce := uuid.New().String() 13 | nonceHash, err := crypto.EncryptAES(nonce) 14 | if err != nil { 15 | return "", "", err 16 | } 17 | return nonce, nonceHash, err 18 | } 19 | 20 | // EncryptNonce nonce string 21 | func EncryptNonce(nonce string) (string, error) { 22 | nonceHash, err := crypto.EncryptAES(nonce) 23 | if err != nil { 24 | return "", err 25 | } 26 | return nonceHash, err 27 | } 28 | 29 | // DecryptNonce nonce string 30 | func DecryptNonce(nonceHash string) (string, error) { 31 | nonce, err := crypto.DecryptAES(nonceHash) 32 | if err != nil { 33 | return "", err 34 | } 35 | return nonce, err 36 | } 37 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rm -rf build && NODE_ENV=production node ./esbuild.config.js", 8 | "start": "NODE_ENV=development node ./esbuild.config.js", 9 | "format": "prettier --write 'src/**/*.(ts|tsx|js|jsx)'" 10 | }, 11 | "keywords": [], 12 | "author": "Lakhan Samani", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@authorizerdev/authorizer-react": "^1.3.2", 16 | "@types/react": "^17.0.15", 17 | "@types/react-dom": "^17.0.9", 18 | "esbuild": "^0.12.17", 19 | "react": "^17.0.2", 20 | "react-dom": "^17.0.2", 21 | "react-is": "^17.0.2", 22 | "react-router-dom": "^5.2.0", 23 | "styled-components": "^5.3.0", 24 | "typescript": "^4.3.5" 25 | }, 26 | "devDependencies": { 27 | "@types/react-router-dom": "^5.1.8", 28 | "@types/styled-components": "^5.1.11", 29 | "prettier": "2.7.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/token/verification_token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/authorizerdev/authorizer/server/constants" 7 | "github.com/authorizerdev/authorizer/server/memorystore" 8 | "github.com/golang-jwt/jwt" 9 | ) 10 | 11 | // CreateVerificationToken creates a verification JWT token 12 | func CreateVerificationToken(email, tokenType, hostname, nonceHash, redirectURL string) (string, error) { 13 | clientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyClientID) 14 | if err != nil { 15 | return "", err 16 | } 17 | claims := jwt.MapClaims{ 18 | "iss": hostname, 19 | "aud": clientID, 20 | "sub": email, 21 | "exp": time.Now().Add(time.Minute * 30).Unix(), 22 | "iat": time.Now().Unix(), 23 | "token_type": tokenType, 24 | "nonce": nonceHash, 25 | "redirect_uri": redirectURL, 26 | } 27 | 28 | return SignJWTToken(claims) 29 | } 30 | -------------------------------------------------------------------------------- /server/db/models/session.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Note: any change here should be reflected in providers/casandra/provider.go as it does not have model support in collection creation 4 | 5 | // Session model for db 6 | type Session struct { 7 | Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty" dynamo:"key,omitempty"` // for arangodb 8 | ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id" dynamo:"id,hash"` 9 | UserID string `gorm:"type:char(36)" json:"user_id" bson:"user_id" cql:"user_id" dynamo:"user_id" index:"user_id,hash"` 10 | UserAgent string `json:"user_agent" bson:"user_agent" cql:"user_agent" dynamo:"user_agent"` 11 | IP string `json:"ip" bson:"ip" cql:"ip" dynamo:"ip"` 12 | CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at" dynamo:"created_at"` 13 | UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at" dynamo:"updated_at"` 14 | } 15 | -------------------------------------------------------------------------------- /server/db/providers/couchbase/session.go: -------------------------------------------------------------------------------- 1 | package couchbase 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/authorizerdev/authorizer/server/db/models" 8 | "github.com/couchbase/gocb/v2" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | // AddSession to save session information in database 13 | func (p *provider) AddSession(ctx context.Context, session *models.Session) error { 14 | if session.ID == "" { 15 | session.ID = uuid.New().String() 16 | } 17 | session.CreatedAt = time.Now().Unix() 18 | session.UpdatedAt = time.Now().Unix() 19 | insertOpt := gocb.InsertOptions{ 20 | Context: ctx, 21 | } 22 | _, err := p.db.Collection(models.Collections.Session).Insert(session.ID, session, &insertOpt) 23 | if err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | 29 | // DeleteSession to delete session information from database 30 | func (p *provider) DeleteSession(ctx context.Context, userId string) error { 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /server/authenticators/totp_store.go: -------------------------------------------------------------------------------- 1 | package authenticators 2 | 3 | import ( 4 | "github.com/authorizerdev/authorizer/server/authenticators/providers" 5 | "github.com/authorizerdev/authorizer/server/authenticators/providers/totp" 6 | "github.com/authorizerdev/authorizer/server/constants" 7 | "github.com/authorizerdev/authorizer/server/memorystore" 8 | ) 9 | 10 | // Provider is the global authenticators provider. 11 | var Provider providers.Provider 12 | 13 | // InitTOTPStore initializes the TOTP authenticator store if it's not disabled in the environment variables. 14 | // It sets the global Provider variable to a new TOTP provider. 15 | func InitTOTPStore() error { 16 | var err error 17 | isTOTPEnvServiceDisabled, _ := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableTOTPLogin) 18 | 19 | if !isTOTPEnvServiceDisabled { 20 | Provider, err = totp.NewProvider() 21 | if err != nil { 22 | return err 23 | } 24 | } 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /server/db/providers/provider_template/env.go: -------------------------------------------------------------------------------- 1 | package provider_template 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/authorizerdev/authorizer/server/db/models" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // AddEnv to save environment information in database 12 | func (p *provider) AddEnv(ctx context.Context, env *models.Env) (*models.Env, error) { 13 | if env.ID == "" { 14 | env.ID = uuid.New().String() 15 | } 16 | 17 | env.CreatedAt = time.Now().Unix() 18 | env.UpdatedAt = time.Now().Unix() 19 | return env, nil 20 | } 21 | 22 | // UpdateEnv to update environment information in database 23 | func (p *provider) UpdateEnv(ctx context.Context, env *models.Env) (*models.Env, error) { 24 | env.UpdatedAt = time.Now().Unix() 25 | return env, nil 26 | } 27 | 28 | // GetEnv to get environment information from database 29 | func (p *provider) GetEnv(ctx context.Context) (*models.Env, error) { 30 | var env *models.Env 31 | 32 | return env, nil 33 | } 34 | -------------------------------------------------------------------------------- /server/handlers/jwks.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gin-gonic/gin" 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/authorizerdev/authorizer/server/constants" 10 | "github.com/authorizerdev/authorizer/server/memorystore" 11 | ) 12 | 13 | func JWKsHandler() gin.HandlerFunc { 14 | return func(c *gin.Context) { 15 | var data map[string]string 16 | jwk, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyJWK) 17 | if err != nil { 18 | log.Debug("Error getting JWK from memorystore: ", err) 19 | c.JSON(500, gin.H{ 20 | "error": err.Error(), 21 | }) 22 | return 23 | } 24 | err = json.Unmarshal([]byte(jwk), &data) 25 | if err != nil { 26 | log.Debug("Failed to parse JWK: ", err) 27 | c.JSON(500, gin.H{ 28 | "error": err.Error(), 29 | }) 30 | return 31 | } 32 | c.JSON(200, gin.H{ 33 | "keys": []map[string]string{ 34 | data, 35 | }, 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/db/providers/mongodb/session.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/authorizerdev/authorizer/server/db/models" 8 | "github.com/google/uuid" 9 | "go.mongodb.org/mongo-driver/mongo/options" 10 | ) 11 | 12 | // AddSession to save session information in database 13 | func (p *provider) AddSession(ctx context.Context, session *models.Session) error { 14 | if session.ID == "" { 15 | session.ID = uuid.New().String() 16 | } 17 | 18 | session.Key = session.ID 19 | session.CreatedAt = time.Now().Unix() 20 | session.UpdatedAt = time.Now().Unix() 21 | sessionCollection := p.db.Collection(models.Collections.Session, options.Collection()) 22 | _, err := sessionCollection.InsertOne(ctx, session) 23 | if err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | 29 | // DeleteSession to delete session information from database 30 | func (p *provider) DeleteSession(ctx context.Context, userId string) error { 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /server/middlewares/client_check.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/authorizerdev/authorizer/server/constants" 10 | "github.com/authorizerdev/authorizer/server/memorystore" 11 | ) 12 | 13 | // ClientCheckMiddleware is a middleware to verify the client ID 14 | // Note: client ID is passed in the header 15 | func ClientCheckMiddleware() gin.HandlerFunc { 16 | return func(c *gin.Context) { 17 | clientID := c.Request.Header.Get("X-Authorizer-Client-ID") 18 | if client, _ := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyClientID); clientID != "" && client != "" && client != clientID { 19 | log.Debug("Client ID is invalid: ", clientID) 20 | c.JSON(http.StatusBadRequest, gin.H{ 21 | "error": "invalid_client_id", 22 | "error_description": "The client id is invalid", 23 | }) 24 | return 25 | } 26 | 27 | c.Next() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /server/test/admin_login_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/authorizerdev/authorizer/server/constants" 7 | "github.com/authorizerdev/authorizer/server/graph/model" 8 | "github.com/authorizerdev/authorizer/server/memorystore" 9 | "github.com/authorizerdev/authorizer/server/resolvers" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func adminLoginTests(t *testing.T, s TestSetup) { 14 | t.Helper() 15 | t.Run(`should complete admin login`, func(t *testing.T) { 16 | _, ctx := createContext(s) 17 | _, err := resolvers.AdminLoginResolver(ctx, model.AdminLoginInput{ 18 | AdminSecret: "admin_test", 19 | }) 20 | 21 | assert.NotNil(t, err) 22 | 23 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 24 | assert.Nil(t, err) 25 | _, err = resolvers.AdminLoginResolver(ctx, model.AdminLoginInput{ 26 | AdminSecret: adminSecret, 27 | }) 28 | 29 | assert.Nil(t, err) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /server/test/admin_signup_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/authorizerdev/authorizer/server/constants" 7 | "github.com/authorizerdev/authorizer/server/graph/model" 8 | "github.com/authorizerdev/authorizer/server/memorystore" 9 | "github.com/authorizerdev/authorizer/server/resolvers" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func adminSignupTests(t *testing.T, s TestSetup) { 14 | t.Helper() 15 | t.Run(`should complete admin signup`, func(t *testing.T) { 16 | _, ctx := createContext(s) 17 | _, err := resolvers.AdminSignupResolver(ctx, model.AdminSignupInput{ 18 | AdminSecret: "admin", 19 | }) 20 | assert.NotNil(t, err) 21 | // reset env for test to pass 22 | err = memorystore.Provider.UpdateEnvVariable(constants.EnvKeyAdminSecret, "") 23 | assert.Nil(t, err) 24 | _, err = resolvers.AdminSignupResolver(ctx, model.AdminSignupInput{ 25 | AdminSecret: "admin123", 26 | }) 27 | assert.NoError(t, err) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /server/resolvers/admin_logout.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/authorizerdev/authorizer/server/cookie" 10 | "github.com/authorizerdev/authorizer/server/graph/model" 11 | "github.com/authorizerdev/authorizer/server/token" 12 | "github.com/authorizerdev/authorizer/server/utils" 13 | ) 14 | 15 | // AdminLogoutResolver is a resolver for admin logout mutation 16 | func AdminLogoutResolver(ctx context.Context) (*model.Response, error) { 17 | var res *model.Response 18 | 19 | gc, err := utils.GinContextFromContext(ctx) 20 | if err != nil { 21 | log.Debug("Failed to get GinContext: ", err) 22 | return res, err 23 | } 24 | 25 | if !token.IsSuperAdmin(gc) { 26 | log.Debug("Admin is not logged in") 27 | return res, fmt.Errorf("unauthorized") 28 | } 29 | 30 | cookie.DeleteAdminCookie(gc) 31 | 32 | res = &model.Response{ 33 | Message: "admin logged out successfully", 34 | } 35 | return res, nil 36 | } 37 | -------------------------------------------------------------------------------- /server/middlewares/cors.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/authorizerdev/authorizer/server/validators" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | // CORSMiddleware is a middleware to add cors headers 9 | func CORSMiddleware() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | origin := c.Request.Header.Get("Origin") 12 | if validators.IsValidOrigin(origin) { 13 | c.Writer.Header().Set("Access-Control-Allow-Origin", origin) 14 | } 15 | 16 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 17 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, X-authorizer-url, X-Forwarded-Proto, X-authorizer-client-id") 18 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") 19 | 20 | if c.Request.Method == "OPTIONS" { 21 | c.AbortWithStatus(204) 22 | return 23 | } 24 | 25 | c.Next() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAuthorizer } from '@authorizerdev/authorizer-react'; 3 | 4 | export default function Dashboard() { 5 | const [loading, setLoading] = React.useState(false); 6 | const { user, setToken, authorizerRef } = useAuthorizer(); 7 | 8 | const onLogout = async () => { 9 | setLoading(true); 10 | await authorizerRef.logout(); 11 | setToken(null); 12 | setLoading(false); 13 | }; 14 | 15 | return ( 16 |
17 |

Hey 👋,

18 |

Thank you for using authorizer.

19 |

20 | Your email address is{' '} 21 | 22 | {user?.email} 23 | 24 |

25 | 26 |
27 | {loading ? ( 28 |

Processing....

29 | ) : ( 30 |

37 | Logout 38 |

39 | )} 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /server/test/admin_logout_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/crypto" 9 | "github.com/authorizerdev/authorizer/server/memorystore" 10 | "github.com/authorizerdev/authorizer/server/resolvers" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func adminLogoutTests(t *testing.T, s TestSetup) { 15 | t.Helper() 16 | t.Run(`should get admin session`, func(t *testing.T) { 17 | req, ctx := createContext(s) 18 | _, err := resolvers.AdminLogoutResolver(ctx) 19 | assert.NotNil(t, err) 20 | 21 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 22 | assert.Nil(t, err) 23 | 24 | h, err := crypto.EncryptPassword(adminSecret) 25 | assert.Nil(t, err) 26 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 27 | _, err = resolvers.AdminLogoutResolver(ctx) 28 | 29 | assert.Nil(t, err) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /server/resolvers/webhook.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/authorizerdev/authorizer/server/db" 8 | "github.com/authorizerdev/authorizer/server/graph/model" 9 | "github.com/authorizerdev/authorizer/server/token" 10 | "github.com/authorizerdev/authorizer/server/utils" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // WebhookResolver resolver for getting webhook by identifier 15 | func WebhookResolver(ctx context.Context, params model.WebhookRequest) (*model.Webhook, error) { 16 | gc, err := utils.GinContextFromContext(ctx) 17 | if err != nil { 18 | log.Debug("Failed to get GinContext: ", err) 19 | return nil, err 20 | } 21 | 22 | if !token.IsSuperAdmin(gc) { 23 | log.Debug("Not logged in as super admin") 24 | return nil, fmt.Errorf("unauthorized") 25 | } 26 | 27 | webhook, err := db.Provider.GetWebhookByID(ctx, params.ID) 28 | if err != nil { 29 | log.Debug("error getting webhook: ", err) 30 | return nil, err 31 | } 32 | return webhook, nil 33 | } 34 | -------------------------------------------------------------------------------- /server/test/admin_session_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/crypto" 9 | "github.com/authorizerdev/authorizer/server/memorystore" 10 | "github.com/authorizerdev/authorizer/server/resolvers" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func adminSessionTests(t *testing.T, s TestSetup) { 15 | t.Helper() 16 | t.Run(`should get admin session`, func(t *testing.T) { 17 | req, ctx := createContext(s) 18 | _, err := resolvers.AdminSessionResolver(ctx) 19 | assert.NotNil(t, err) 20 | 21 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 22 | assert.Nil(t, err) 23 | 24 | h, err := crypto.EncryptPassword(adminSecret) 25 | assert.Nil(t, err) 26 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 27 | _, err = resolvers.AdminSessionResolver(ctx) 28 | 29 | assert.Nil(t, err) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /server/db/models/env.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Note: any change here should be reflected in providers/casandra/provider.go as it does not have model support in collection creation 4 | 5 | // Env model for db 6 | type Env struct { 7 | Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty" dynamo:"key,omitempty"` // for arangodb 8 | ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id" dynamo:"id,hash"` 9 | EnvData string `json:"env" bson:"env" cql:"env" dynamo:"env"` 10 | Hash string `json:"hash" bson:"hash" cql:"hash" dynamo:"hash"` 11 | EncryptionKey string `json:"encryption_key" bson:"encryption_key" cql:"encryption_key" dynamo:"encryption_key"` // couchbase has "hash" as reserved keyword so we cannot use it. This will be empty for other dbs. 12 | UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at" dynamo:"updated_at"` 13 | CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at" dynamo:"created_at"` 14 | } 15 | -------------------------------------------------------------------------------- /server/memorystore/providers/inmemory/stores/state_store.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // StateStore struct to store the env variables 8 | type StateStore struct { 9 | mutex sync.Mutex 10 | store map[string]string 11 | } 12 | 13 | // NewStateStore create a new state store 14 | func NewStateStore() *StateStore { 15 | return &StateStore{ 16 | mutex: sync.Mutex{}, 17 | store: make(map[string]string), 18 | } 19 | } 20 | 21 | // Get returns the value of the key in state store 22 | func (s *StateStore) Get(key string) string { 23 | s.mutex.Lock() 24 | defer s.mutex.Unlock() 25 | return s.store[key] 26 | } 27 | 28 | // Set sets the value of the key in state store 29 | func (s *StateStore) Set(key string, value string) { 30 | s.mutex.Lock() 31 | defer s.mutex.Unlock() 32 | 33 | s.store[key] = value 34 | } 35 | 36 | // Remove removes the key from state store 37 | func (s *StateStore) Remove(key string) { 38 | s.mutex.Lock() 39 | defer s.mutex.Unlock() 40 | 41 | delete(s.store, key) 42 | } 43 | -------------------------------------------------------------------------------- /server/test/env_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/crypto" 9 | "github.com/authorizerdev/authorizer/server/memorystore" 10 | "github.com/authorizerdev/authorizer/server/resolvers" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func envTests(t *testing.T, s TestSetup) { 15 | t.Helper() 16 | t.Run(`should get envs`, func(t *testing.T) { 17 | req, ctx := createContext(s) 18 | _, err := resolvers.EnvResolver(ctx) 19 | assert.NotNil(t, err) 20 | 21 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 22 | assert.Nil(t, err) 23 | 24 | h, err := crypto.EncryptPassword(adminSecret) 25 | assert.Nil(t, err) 26 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 27 | res, err := resolvers.EnvResolver(ctx) 28 | assert.Nil(t, err) 29 | assert.Equal(t, *res.AdminSecret, adminSecret) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /server/db/providers/dynamodb/shared.go: -------------------------------------------------------------------------------- 1 | package dynamodb 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" 5 | "github.com/guregu/dynamo" 6 | ) 7 | 8 | // As updpate all item not supported so set manually via Set and SetNullable for empty field 9 | func UpdateByHashKey(table dynamo.Table, hashKey string, hashValue string, item interface{}) error { 10 | existingValue, err := dynamo.MarshalItem(item) 11 | var i interface{} 12 | if err != nil { 13 | return err 14 | } 15 | nullableValue, err := dynamodbattribute.MarshalMap(item) 16 | if err != nil { 17 | return err 18 | } 19 | u := table.Update(hashKey, hashValue) 20 | for k, v := range existingValue { 21 | if k == hashKey { 22 | continue 23 | } 24 | u = u.Set(k, v) 25 | } 26 | for k, v := range nullableValue { 27 | if k == hashKey { 28 | continue 29 | } 30 | dynamodbattribute.Unmarshal(v, &i) 31 | if i == nil { 32 | u = u.SetNullable(k, v) 33 | } 34 | } 35 | err = u.Run() 36 | if err != nil { 37 | return err 38 | } 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /scripts/build-mac.sh: -------------------------------------------------------------------------------- 1 | VERSION="$1" 2 | make clean && make build-app && CGO_ENABLED=1 VERSION=${VERSION} make 3 | FILE_NAME=authorizer-${VERSION}-darwin-amd64.tar.gz 4 | tar cvfz ${FILE_NAME} .env app/build build templates dashboard/build 5 | AUTH="Authorization: token $GITHUB_TOKEN" 6 | RELASE_INFO=$(curl -sH "$AUTH" https://api.github.com/repos/authorizerdev/authorizer/releases/tags/${VERSION}) 7 | echo $RELASE_INFO 8 | 9 | eval $(echo "$RELASE_INFO" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=') 10 | [ "$id" ] || { echo "Error: Failed to get release id for tag: $VERSION"; echo "$RELASE_INFO" | awk 'length($0)<100' >&2; exit 1; } 11 | echo $id 12 | GH_ASSET="https://uploads.github.com/repos/authorizerdev/authorizer/releases/$id/assets?name=$(basename $FILE_NAME)" 13 | 14 | echo $GH_ASSET 15 | 16 | curl -H $AUTH -H "Content-Type: $(file -b --mime-type $FILE_NAME)" --data-binary @$FILE_NAME $GH_ASSET 17 | 18 | curl "$GITHUB_OAUTH_BASIC" --data-binary @"$FILE_NAME" -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/octet-stream" $GH_ASSET 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Version:** x.y.z 10 | 11 | 12 | 13 | **Describe the bug** 14 | 15 | 16 | 17 | **Steps To Reproduce** 18 | 19 | 20 | 21 | **Expected behavior** 22 | 23 | 24 | 25 | **Screenshots** 26 | 27 | 28 | 29 | **Desktop (please complete the following information):** 30 | 31 | - OS: [e.g. iOS] 32 | - Browser [e.g. chrome, safari] 33 | - Version [e.g. 22] 34 | 35 | **Additional context** 36 | 37 | 38 | -------------------------------------------------------------------------------- /server/utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | ) 7 | 8 | // CreateFolder creates a folder in Current working dir 9 | func CreateFolder(dir string) (string, error) { 10 | pwd, err := os.Getwd() 11 | if err != nil { 12 | return "", err 13 | } 14 | path := pwd + "/" + dir 15 | err = os.Mkdir(path, 0o755) 16 | if err == nil { 17 | return path, nil 18 | } 19 | if os.IsExist(err) { 20 | // check that the existing path is a directory 21 | info, err := os.Stat(path) 22 | if err != nil { 23 | return "", err 24 | } 25 | if !info.IsDir() { 26 | return "", errors.New("path exists but is not a directory") 27 | } 28 | return path, nil 29 | } 30 | return path, err 31 | } 32 | 33 | // CreateFile creates a file on given path with given content 34 | func CreateFile(filePath string, content string) error { 35 | f, err := os.Create(filePath) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | defer f.Close() 41 | 42 | _, err = f.WriteString(content) 43 | 44 | if err != nil { 45 | return err 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /server/constants/oauth2.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // - query: for Authorization Code grant. 302 Found triggers redirect. 5 | ResponseModeQuery = "query" 6 | // - fragment: for Implicit grant. 302 Found triggers redirect. 7 | ResponseModeFragment = "fragment" 8 | // - form_post: 200 OK with response parameters embedded in an HTML form as hidden parameters. 9 | ResponseModeFormPost = "form_post" 10 | // - web_message: For Silent Authentication. Uses HTML5 web messaging. 11 | ResponseModeWebMessage = "web_message" 12 | 13 | // For the Authorization Code grant, use response_type=code to include the authorization code. 14 | ResponseTypeCode = "code" 15 | // For the Implicit grant, use response_type=token to include an access token. 16 | ResponseTypeToken = "token" 17 | // For the Implicit grant of id_token, use response_type=id_token to include an identifier token. 18 | ResponseTypeIDToken = "id_token" 19 | 20 | // Constant indicating the "signup" screen hint for customizing authentication process and redirect to a signup page. 21 | ScreenHintSignUp = "signup" 22 | ) 23 | -------------------------------------------------------------------------------- /server/resolvers/users.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/authorizerdev/authorizer/server/db" 10 | "github.com/authorizerdev/authorizer/server/graph/model" 11 | "github.com/authorizerdev/authorizer/server/token" 12 | "github.com/authorizerdev/authorizer/server/utils" 13 | ) 14 | 15 | // UsersResolver is a resolver for users query 16 | // This is admin only query 17 | func UsersResolver(ctx context.Context, params *model.PaginatedInput) (*model.Users, error) { 18 | gc, err := utils.GinContextFromContext(ctx) 19 | if err != nil { 20 | log.Debug("Failed to get GinContext: ", err) 21 | return nil, err 22 | } 23 | 24 | if !token.IsSuperAdmin(gc) { 25 | log.Debug("Not logged in as super admin.") 26 | return nil, fmt.Errorf("unauthorized") 27 | } 28 | 29 | pagination := utils.GetPagination(params) 30 | 31 | res, err := db.Provider.ListUsers(ctx, pagination) 32 | if err != nil { 33 | log.Debug("Failed to get users: ", err) 34 | return nil, err 35 | } 36 | 37 | return res, nil 38 | } 39 | -------------------------------------------------------------------------------- /server/authenticators/providers/providers.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import "context" 4 | 5 | // AuthenticatorConfig defines authenticator config 6 | type AuthenticatorConfig struct { 7 | // ScannerImage is the base64 of QR code image 8 | ScannerImage string 9 | // Secrets is the secret key 10 | Secret string 11 | // RecoveryCode is the list of recovery codes 12 | RecoveryCodes []string 13 | // RecoveryCodeMap is the map of recovery codes 14 | RecoveryCodeMap map[string]bool 15 | } 16 | 17 | // Provider defines authenticators provider 18 | type Provider interface { 19 | // Generate totp: to generate totp, store secret into db and returns base64 of QR code image 20 | Generate(ctx context.Context, id string) (*AuthenticatorConfig, error) 21 | // Validate totp: user passcode with secret stored in our db 22 | Validate(ctx context.Context, passcode string, userID string) (bool, error) 23 | // ValidateRecoveryCode totp: allows user to validate using recovery code incase if they lost their device 24 | ValidateRecoveryCode(ctx context.Context, recoveryCode, userID string) (bool, error) 25 | } 26 | -------------------------------------------------------------------------------- /server/resolvers/webhooks.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/authorizerdev/authorizer/server/db" 8 | "github.com/authorizerdev/authorizer/server/graph/model" 9 | "github.com/authorizerdev/authorizer/server/token" 10 | "github.com/authorizerdev/authorizer/server/utils" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // WebhooksResolver resolver for getting the list of webhooks based on pagination 15 | func WebhooksResolver(ctx context.Context, params *model.PaginatedInput) (*model.Webhooks, error) { 16 | gc, err := utils.GinContextFromContext(ctx) 17 | if err != nil { 18 | log.Debug("Failed to get GinContext: ", err) 19 | return nil, err 20 | } 21 | 22 | if !token.IsSuperAdmin(gc) { 23 | log.Debug("Not logged in as super admin") 24 | return nil, fmt.Errorf("unauthorized") 25 | } 26 | 27 | pagination := utils.GetPagination(params) 28 | webhooks, err := db.Provider.ListWebhook(ctx, pagination) 29 | if err != nil { 30 | log.Debug("failed to get webhooks: ", err) 31 | return nil, err 32 | } 33 | return webhooks, nil 34 | } 35 | -------------------------------------------------------------------------------- /server/db/providers/cassandradb/session.go: -------------------------------------------------------------------------------- 1 | package cassandradb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/authorizerdev/authorizer/server/db/models" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | // AddSession to save session information in database 13 | func (p *provider) AddSession(ctx context.Context, session *models.Session) error { 14 | if session.ID == "" { 15 | session.ID = uuid.New().String() 16 | } 17 | session.CreatedAt = time.Now().Unix() 18 | session.UpdatedAt = time.Now().Unix() 19 | insertSessionQuery := fmt.Sprintf("INSERT INTO %s (id, user_id, user_agent, ip, created_at, updated_at) VALUES ('%s', '%s', '%s', '%s', %d, %d)", KeySpace+"."+models.Collections.Session, session.ID, session.UserID, session.UserAgent, session.IP, session.CreatedAt, session.UpdatedAt) 20 | err := p.db.Query(insertSessionQuery).Exec() 21 | if err != nil { 22 | return err 23 | } 24 | return nil 25 | } 26 | 27 | // DeleteSession to delete session information from database 28 | func (p *provider) DeleteSession(ctx context.Context, userId string) error { 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /dashboard/src/layouts/DashboardLayout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Drawer, 4 | DrawerContent, 5 | useDisclosure, 6 | useColorModeValue, 7 | } from '@chakra-ui/react'; 8 | import React, { ReactNode } from 'react'; 9 | import { Sidebar, MobileNav } from '../components/Menu'; 10 | 11 | export function DashboardLayout({ children }: { children: ReactNode }) { 12 | const { isOpen, onOpen, onClose } = useDisclosure(); 13 | return ( 14 | 15 | onClose} 17 | display={{ base: 'none', md: 'block' }} 18 | /> 19 | 28 | 29 | 30 | 31 | 32 | {/* mobilenav */} 33 | 34 | 35 | {children} 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /server/test/email_templates_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/crypto" 9 | "github.com/authorizerdev/authorizer/server/memorystore" 10 | "github.com/authorizerdev/authorizer/server/resolvers" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func emailTemplatesTest(t *testing.T, s TestSetup) { 15 | t.Helper() 16 | t.Run("should get email templates", func(t *testing.T) { 17 | req, ctx := createContext(s) 18 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 19 | assert.NoError(t, err) 20 | h, err := crypto.EncryptPassword(adminSecret) 21 | assert.NoError(t, err) 22 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 23 | 24 | emailTemplates, err := resolvers.EmailTemplatesResolver(ctx, nil) 25 | assert.NoError(t, err) 26 | assert.NotEmpty(t, emailTemplates) 27 | assert.Len(t, emailTemplates.EmailTemplates, len(s.TestInfo.TestEmailTemplateEventTypes)) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /server/test/resend_verify_email_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/authorizerdev/authorizer/server/constants" 7 | "github.com/authorizerdev/authorizer/server/graph/model" 8 | "github.com/authorizerdev/authorizer/server/refs" 9 | "github.com/authorizerdev/authorizer/server/resolvers" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func resendVerifyEmailTests(t *testing.T, s TestSetup) { 14 | t.Helper() 15 | t.Run(`should resend verification email`, func(t *testing.T) { 16 | _, ctx := createContext(s) 17 | email := "resend_verify_email." + s.TestInfo.Email 18 | _, err := resolvers.SignupResolver(ctx, model.SignUpInput{ 19 | Email: refs.NewStringRef(email), 20 | Password: s.TestInfo.Password, 21 | ConfirmPassword: s.TestInfo.Password, 22 | }) 23 | assert.NoError(t, err) 24 | _, err = resolvers.ResendVerifyEmailResolver(ctx, model.ResendVerifyEmailInput{ 25 | Email: email, 26 | Identifier: constants.VerificationTypeBasicAuthSignup, 27 | }) 28 | assert.NoError(t, err) 29 | 30 | cleanData(email) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 authorizer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/resolvers/profile.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | 6 | log "github.com/sirupsen/logrus" 7 | 8 | "github.com/authorizerdev/authorizer/server/db" 9 | "github.com/authorizerdev/authorizer/server/graph/model" 10 | "github.com/authorizerdev/authorizer/server/token" 11 | "github.com/authorizerdev/authorizer/server/utils" 12 | ) 13 | 14 | // ProfileResolver is a resolver for profile query 15 | func ProfileResolver(ctx context.Context) (*model.User, error) { 16 | var res *model.User 17 | 18 | gc, err := utils.GinContextFromContext(ctx) 19 | if err != nil { 20 | log.Debug("Failed to get GinContext: ", err) 21 | return res, err 22 | } 23 | tokenData, err := token.GetUserIDFromSessionOrAccessToken(gc) 24 | if err != nil { 25 | log.Debug("Failed GetUserIDFromSessionOrAccessToken: ", err) 26 | return res, err 27 | } 28 | log := log.WithFields(log.Fields{ 29 | "user_id": tokenData.UserID, 30 | }) 31 | user, err := db.Provider.GetUserByID(ctx, tokenData.UserID) 32 | if err != nil { 33 | log.Debug("Failed to get user: ", err) 34 | return res, err 35 | } 36 | 37 | return user.AsAPIUser(), nil 38 | } 39 | -------------------------------------------------------------------------------- /server/constants/verification_types.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // VerificationTypeBasicAuthSignup is the basic_auth_signup verification type 5 | VerificationTypeBasicAuthSignup = "basic_auth_signup" 6 | // VerificationTypeMagicLinkLogin is the magic_link_login verification type 7 | VerificationTypeMagicLinkLogin = "magic_link_login" 8 | // VerificationTypeUpdateEmail is the update_email verification type 9 | VerificationTypeUpdateEmail = "update_email" 10 | // VerificationTypeForgotPassword is the forgot_password verification type 11 | VerificationTypeForgotPassword = "forgot_password" 12 | // VerificationTypeInviteMember is the invite_member verification type 13 | VerificationTypeInviteMember = "invite_member" 14 | // VerificationTypeOTP is the otp verification type 15 | VerificationTypeOTP = "verify_otp" 16 | ) 17 | 18 | var ( 19 | // VerificationTypes is slice of all verification types 20 | VerificationTypes = []string{ 21 | VerificationTypeBasicAuthSignup, 22 | VerificationTypeMagicLinkLogin, 23 | VerificationTypeUpdateEmail, 24 | VerificationTypeForgotPassword, 25 | VerificationTypeInviteMember, 26 | } 27 | ) 28 | -------------------------------------------------------------------------------- /server/resolvers/email_templates.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/authorizerdev/authorizer/server/db" 8 | "github.com/authorizerdev/authorizer/server/graph/model" 9 | "github.com/authorizerdev/authorizer/server/token" 10 | "github.com/authorizerdev/authorizer/server/utils" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // EmailTemplatesResolver resolver for getting the list of email templates based on pagination 15 | func EmailTemplatesResolver(ctx context.Context, params *model.PaginatedInput) (*model.EmailTemplates, error) { 16 | gc, err := utils.GinContextFromContext(ctx) 17 | if err != nil { 18 | log.Debug("Failed to get GinContext: ", err) 19 | return nil, err 20 | } 21 | 22 | if !token.IsSuperAdmin(gc) { 23 | log.Debug("Not logged in as super admin") 24 | return nil, fmt.Errorf("unauthorized") 25 | } 26 | 27 | pagination := utils.GetPagination(params) 28 | emailTemplates, err := db.Provider.ListEmailTemplate(ctx, pagination) 29 | if err != nil { 30 | log.Debug("failed to get email templates: ", err) 31 | return nil, err 32 | } 33 | return emailTemplates, nil 34 | } 35 | -------------------------------------------------------------------------------- /server/resolvers/verification_requests.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/authorizerdev/authorizer/server/db" 10 | "github.com/authorizerdev/authorizer/server/graph/model" 11 | "github.com/authorizerdev/authorizer/server/token" 12 | "github.com/authorizerdev/authorizer/server/utils" 13 | ) 14 | 15 | // VerificationRequestsResolver is a resolver for verification requests query 16 | // This is admin only query 17 | func VerificationRequestsResolver(ctx context.Context, params *model.PaginatedInput) (*model.VerificationRequests, error) { 18 | gc, err := utils.GinContextFromContext(ctx) 19 | if err != nil { 20 | log.Debug("Failed to get GinContext: ", err) 21 | return nil, err 22 | } 23 | 24 | if !token.IsSuperAdmin(gc) { 25 | log.Debug("Not logged in as super admin") 26 | return nil, fmt.Errorf("unauthorized") 27 | } 28 | 29 | pagination := utils.GetPagination(params) 30 | res, err := db.Provider.ListVerificationRequests(ctx, pagination) 31 | if err != nil { 32 | log.Debug("Failed to get verification requests: ", err) 33 | return nil, err 34 | } 35 | 36 | return res, nil 37 | } 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.25.5-alpine3.22 as go-builder 2 | WORKDIR /authorizer 3 | COPY server server 4 | COPY Makefile . 5 | 6 | ARG VERSION="latest" 7 | ENV VERSION="$VERSION" 8 | 9 | RUN echo "$VERSION" 10 | RUN apk add build-base &&\ 11 | make clean && make && \ 12 | chmod 777 build/server 13 | 14 | FROM node:24.12.0-alpine3.22 as node-builder 15 | WORKDIR /authorizer 16 | COPY app app 17 | COPY dashboard dashboard 18 | COPY Makefile . 19 | RUN apk add build-base &&\ 20 | make build-app && \ 21 | make build-dashboard 22 | 23 | FROM alpine:3.22 24 | RUN adduser -D -h /authorizer -u 1000 -k /dev/null authorizer 25 | WORKDIR /authorizer 26 | RUN mkdir app dashboard 27 | COPY --from=node-builder --chown=nobody:nobody /authorizer/app/build app/build 28 | COPY --from=node-builder --chown=nobody:nobody /authorizer/app/favicon_io app/favicon_io 29 | COPY --from=node-builder --chown=nobody:nobody /authorizer/dashboard/build dashboard/build 30 | COPY --from=node-builder --chown=nobody:nobody /authorizer/dashboard/favicon_io dashboard/favicon_io 31 | COPY --from=go-builder --chown=nobody:nobody /authorizer/build build 32 | COPY templates templates 33 | EXPOSE 8080 34 | USER authorizer 35 | CMD [ "./build/server" ] 36 | -------------------------------------------------------------------------------- /server/db/models/authenticators.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Note: any change here should be reflected in providers/casandra/provider.go as it does not have model support in collection creation 4 | 5 | // Authenticators model for db 6 | type Authenticator struct { 7 | Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty" dynamo:"key,omitempty"` // for arangodb 8 | ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id" dynamo:"id,hash"` 9 | UserID string `gorm:"type:char(36)" json:"user_id" bson:"user_id" cql:"user_id" dynamo:"user_id" index:"user_id,hash"` 10 | Method string `json:"method" bson:"method" cql:"method" dynamo:"method"` 11 | Secret string `json:"secret" bson:"secret" cql:"secret" dynamo:"secret"` 12 | RecoveryCodes *string `json:"recovery_codes" bson:"recovery_codes" cql:"recovery_codes" dynamo:"recovery_codes"` 13 | VerifiedAt *int64 `json:"verified_at" bson:"verified_at" cql:"verified_at" dynamo:"verified_at"` 14 | CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at" dynamo:"created_at"` 15 | UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at" dynamo:"updated_at"` 16 | } 17 | -------------------------------------------------------------------------------- /server/db/models/otp.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | const ( 4 | // FieldName email is the field name for email 5 | FieldNameEmail = "email" 6 | // FieldNamePhoneNumber is the field name for phone number 7 | FieldNamePhoneNumber = "phone_number" 8 | ) 9 | 10 | // OTP model for database 11 | type OTP struct { 12 | Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty" dynamo:"key,omitempty"` // for arangodb 13 | ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id" dynamo:"id,hash"` 14 | Email string `gorm:"index" json:"email" bson:"email" cql:"email" dynamo:"email" index:"email,hash"` 15 | PhoneNumber string `gorm:"index" json:"phone_number" bson:"phone_number" cql:"phone_number" dynamo:"phone_number"` 16 | Otp string `json:"otp" bson:"otp" cql:"otp" dynamo:"otp"` 17 | ExpiresAt int64 `json:"expires_at" bson:"expires_at" cql:"expires_at" dynamo:"expires_at"` 18 | CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at" dynamo:"created_at"` 19 | UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at" dynamo:"updated_at"` 20 | } 21 | 22 | type Paging struct { 23 | ID string `json:"id,omitempty" dynamo:"id,hash"` 24 | } 25 | -------------------------------------------------------------------------------- /dashboard/src/components/EnvComponents/DomainWhitelisting.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Flex, Stack, Center, Text, useMediaQuery } from '@chakra-ui/react'; 3 | import InputField from '../../components/InputField'; 4 | import { ArrayInputType } from '../../constants'; 5 | 6 | const DomainWhiteListing = ({ variables, setVariables }: any) => { 7 | const [isNotSmallerScreen] = useMediaQuery('(min-width:600px)'); 8 | return ( 9 |
10 | {' '} 11 | 12 | Domain White Listing 13 | 14 | 15 | 16 | 17 | Allowed Origins: 18 | 19 |
23 | 28 |
29 |
30 |
31 |
32 | ); 33 | }; 34 | 35 | export default DomainWhiteListing; 36 | -------------------------------------------------------------------------------- /dashboard/src/utils/parseCSV.ts: -------------------------------------------------------------------------------- 1 | import _flatten from 'lodash/flatten'; 2 | import { validateEmail } from '.'; 3 | 4 | interface dataTypes { 5 | value: string; 6 | isInvalid: boolean; 7 | } 8 | 9 | const parseCSV = (file: File, delimiter: string): Promise => { 10 | return new Promise((resolve) => { 11 | const reader = new FileReader(); 12 | 13 | // When the FileReader has loaded the file... 14 | reader.onload = (e: any) => { 15 | // Split the result to an array of lines 16 | const lines = e.target.result.split('\n'); 17 | // Split the lines themselves by the specified 18 | // delimiter, such as a comma 19 | let result = lines.map((line: string) => line.split(delimiter)); 20 | // As the FileReader reads asynchronously, 21 | // we can't just return the result; instead, 22 | // we're passing it to a callback function 23 | result = _flatten(result); 24 | resolve( 25 | result.map((email: string) => { 26 | return { 27 | value: email.trim(), 28 | isInvalid: !validateEmail(email.trim()), 29 | }; 30 | }), 31 | ); 32 | }; 33 | 34 | // Read the file content as a single string 35 | reader.readAsText(file); 36 | }); 37 | }; 38 | 39 | export default parseCSV; 40 | -------------------------------------------------------------------------------- /server/db/providers/provider_template/authenticator.go: -------------------------------------------------------------------------------- 1 | package provider_template 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | 9 | "github.com/authorizerdev/authorizer/server/db/models" 10 | ) 11 | 12 | func (p *provider) AddAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) { 13 | exists, _ := p.GetAuthenticatorDetailsByUserId(ctx, authenticators.UserID, authenticators.Method) 14 | if exists != nil { 15 | return authenticators, nil 16 | } 17 | 18 | if authenticators.ID == "" { 19 | authenticators.ID = uuid.New().String() 20 | } 21 | authenticators.CreatedAt = time.Now().Unix() 22 | authenticators.UpdatedAt = time.Now().Unix() 23 | return authenticators, nil 24 | } 25 | 26 | func (p *provider) UpdateAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) { 27 | authenticators.UpdatedAt = time.Now().Unix() 28 | return authenticators, nil 29 | } 30 | 31 | func (p *provider) GetAuthenticatorDetailsByUserId(ctx context.Context, userId string, authenticatorType string) (*models.Authenticator, error) { 32 | var authenticators *models.Authenticator 33 | return authenticators, nil 34 | } 35 | -------------------------------------------------------------------------------- /server/db/providers/sql/env.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/authorizerdev/authorizer/server/db/models" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // AddEnv to save environment information in database 12 | func (p *provider) AddEnv(ctx context.Context, env *models.Env) (*models.Env, error) { 13 | if env.ID == "" { 14 | env.ID = uuid.New().String() 15 | } 16 | 17 | env.Key = env.ID 18 | env.CreatedAt = time.Now().Unix() 19 | env.UpdatedAt = time.Now().Unix() 20 | 21 | result := p.db.Create(&env) 22 | if result.Error != nil { 23 | return env, result.Error 24 | } 25 | return env, nil 26 | } 27 | 28 | // UpdateEnv to update environment information in database 29 | func (p *provider) UpdateEnv(ctx context.Context, env *models.Env) (*models.Env, error) { 30 | env.UpdatedAt = time.Now().Unix() 31 | result := p.db.Save(&env) 32 | if result.Error != nil { 33 | return env, result.Error 34 | } 35 | return env, nil 36 | } 37 | 38 | // GetEnv to get environment information from database 39 | func (p *provider) GetEnv(ctx context.Context) (*models.Env, error) { 40 | var env *models.Env 41 | result := p.db.First(&env) 42 | if result.Error != nil { 43 | return env, result.Error 44 | } 45 | return env, nil 46 | } 47 | -------------------------------------------------------------------------------- /server/resolvers/logout.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | 6 | log "github.com/sirupsen/logrus" 7 | 8 | "github.com/authorizerdev/authorizer/server/cookie" 9 | "github.com/authorizerdev/authorizer/server/graph/model" 10 | "github.com/authorizerdev/authorizer/server/memorystore" 11 | "github.com/authorizerdev/authorizer/server/token" 12 | "github.com/authorizerdev/authorizer/server/utils" 13 | ) 14 | 15 | // LogoutResolver is a resolver for logout mutation 16 | func LogoutResolver(ctx context.Context) (*model.Response, error) { 17 | gc, err := utils.GinContextFromContext(ctx) 18 | if err != nil { 19 | log.Debug("Failed to get GinContext: ", err) 20 | return nil, err 21 | } 22 | 23 | tokenData, err := token.GetUserIDFromSessionOrAccessToken(gc) 24 | if err != nil { 25 | log.Debug("Failed GetUserIDFromSessionOrAccessToken: ", err) 26 | return nil, err 27 | } 28 | 29 | sessionKey := tokenData.UserID 30 | if tokenData.LoginMethod != "" { 31 | sessionKey = tokenData.LoginMethod + ":" + tokenData.UserID 32 | } 33 | 34 | memorystore.Provider.DeleteUserSession(sessionKey, tokenData.Nonce) 35 | cookie.DeleteSession(gc) 36 | 37 | res := &model.Response{ 38 | Message: "Logged out successfully", 39 | } 40 | 41 | return res, nil 42 | } 43 | -------------------------------------------------------------------------------- /server/db/models/model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // CollectionList / Tables available for authorizer in the database 4 | type CollectionList struct { 5 | User string 6 | VerificationRequest string 7 | Session string 8 | Env string 9 | Webhook string 10 | WebhookLog string 11 | EmailTemplate string 12 | OTP string 13 | SMSVerificationRequest string 14 | Authenticators string 15 | } 16 | 17 | var ( 18 | // Prefix for table name / collection names 19 | Prefix = "authorizer_" 20 | // Collections / Tables available for authorizer in the database (used for dbs other than gorm) 21 | Collections = CollectionList{ 22 | User: Prefix + "users", 23 | VerificationRequest: Prefix + "verification_requests", 24 | Session: Prefix + "sessions", 25 | Env: Prefix + "env", 26 | Webhook: Prefix + "webhooks", 27 | WebhookLog: Prefix + "webhook_logs", 28 | EmailTemplate: Prefix + "email_templates", 29 | OTP: Prefix + "otps", 30 | SMSVerificationRequest: Prefix + "sms_verification_requests", 31 | Authenticators: Prefix + "authenticators", 32 | } 33 | ) 34 | -------------------------------------------------------------------------------- /server/test/webhooks_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/crypto" 9 | "github.com/authorizerdev/authorizer/server/graph/model" 10 | "github.com/authorizerdev/authorizer/server/memorystore" 11 | "github.com/authorizerdev/authorizer/server/refs" 12 | "github.com/authorizerdev/authorizer/server/resolvers" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func webhooksTest(t *testing.T, s TestSetup) { 17 | t.Helper() 18 | t.Run("should get webhooks", func(t *testing.T) { 19 | req, ctx := createContext(s) 20 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 21 | assert.NoError(t, err) 22 | h, err := crypto.EncryptPassword(adminSecret) 23 | assert.NoError(t, err) 24 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 25 | 26 | webhooks, err := resolvers.WebhooksResolver(ctx, &model.PaginatedInput{ 27 | Pagination: &model.PaginationInput{ 28 | Limit: refs.NewInt64Ref(20), 29 | }, 30 | }) 31 | assert.NoError(t, err) 32 | assert.NotEmpty(t, webhooks) 33 | assert.GreaterOrEqual(t, len(webhooks.Webhooks), len(s.TestInfo.TestWebhookEventTypes)*2) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dashboard", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rm -rf build && NODE_ENV=production node ./esbuild.config.js", 8 | "start": "NODE_ENV=development node ./esbuild.config.js", 9 | "format": "prettier --write --use-tabs 'src/**/*.(ts|tsx|js|jsx)'" 10 | }, 11 | "keywords": [], 12 | "author": "Lakhan Samani", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@chakra-ui/react": "^1.7.3", 16 | "@emotion/core": "^11.0.0", 17 | "@emotion/react": "^11.7.1", 18 | "@emotion/styled": "^11.6.0", 19 | "@types/react": "^17.0.38", 20 | "@types/react-dom": "^17.0.11", 21 | "@types/react-router-dom": "^5.3.2", 22 | "dayjs": "^1.10.7", 23 | "esbuild": "^0.14.9", 24 | "focus-visible": "^5.2.0", 25 | "framer-motion": "^5.5.5", 26 | "graphql": "^16.2.0", 27 | "lodash": "^4.17.21", 28 | "react": "^17.0.2", 29 | "react-dom": "^17.0.2", 30 | "react-draft-wysiwyg": "^1.15.0", 31 | "react-dropzone": "^12.0.4", 32 | "react-email-editor": "^1.6.1", 33 | "react-icons": "^4.3.1", 34 | "react-router-dom": "^6.2.1", 35 | "typescript": "^4.5.4", 36 | "urql": "^2.0.6" 37 | }, 38 | "devDependencies": { 39 | "@types/react-email-editor": "^1.1.7", 40 | "prettier": "2.7.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /dashboard/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Fragment } from 'react'; 3 | import { ChakraProvider, extendTheme } from '@chakra-ui/react'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import { createClient, Provider } from 'urql'; 6 | import { AppRoutes } from './routes'; 7 | import { AuthContextProvider } from './contexts/AuthContext'; 8 | 9 | const queryClient = createClient({ 10 | url: '/graphql', 11 | fetchOptions: () => { 12 | return { 13 | credentials: 'include', 14 | headers: { 15 | 'x-authorizer-url': window.location.origin, 16 | }, 17 | }; 18 | }, 19 | requestPolicy: 'network-only', 20 | }); 21 | 22 | const theme = extendTheme({ 23 | styles: { 24 | global: { 25 | 'html, body, #root': { 26 | height: '100%', 27 | outline: 'none', 28 | }, 29 | }, 30 | }, 31 | colors: { 32 | blue: { 33 | 500: 'rgb(59,130,246)', 34 | }, 35 | }, 36 | }); 37 | 38 | export default function App() { 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /server/memorystore/providers/inmemory/stores/env_store.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // EnvStore struct to store the env variables 8 | type EnvStore struct { 9 | mutex sync.Mutex 10 | store map[string]interface{} 11 | } 12 | 13 | // NewEnvStore create a new env store 14 | func NewEnvStore() *EnvStore { 15 | return &EnvStore{ 16 | mutex: sync.Mutex{}, 17 | store: make(map[string]interface{}), 18 | } 19 | } 20 | 21 | // UpdateEnvStore to update the whole env store object 22 | func (e *EnvStore) UpdateStore(store map[string]interface{}) { 23 | e.mutex.Lock() 24 | defer e.mutex.Unlock() 25 | 26 | // just override the keys + new keys 27 | for key, value := range store { 28 | e.store[key] = value 29 | } 30 | } 31 | 32 | // GetStore returns the env store 33 | func (e *EnvStore) GetStore() map[string]interface{} { 34 | e.mutex.Lock() 35 | defer e.mutex.Unlock() 36 | return e.store 37 | } 38 | 39 | // Get returns the value of the key in evn store 40 | func (e *EnvStore) Get(key string) interface{} { 41 | e.mutex.Lock() 42 | defer e.mutex.Unlock() 43 | return e.store[key] 44 | } 45 | 46 | // Set sets the value of the key in env store 47 | func (e *EnvStore) Set(key string, value interface{}) { 48 | e.mutex.Lock() 49 | defer e.mutex.Unlock() 50 | 51 | e.store[key] = value 52 | } 53 | -------------------------------------------------------------------------------- /server/test/env_file_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/authorizerdev/authorizer/server/constants" 10 | "github.com/authorizerdev/authorizer/server/env" 11 | "github.com/authorizerdev/authorizer/server/memorystore" 12 | ) 13 | 14 | func TestEnvs(t *testing.T) { 15 | err := os.Setenv(constants.EnvKeyEnvPath, "../../.env.test") 16 | assert.Nil(t, err) 17 | err = memorystore.InitRequiredEnv() 18 | assert.Nil(t, err) 19 | err = env.InitAllEnv() 20 | assert.Nil(t, err) 21 | store, err := memorystore.Provider.GetEnvStore() 22 | assert.Nil(t, err) 23 | 24 | assert.Equal(t, "test", store[constants.EnvKeyEnv].(string)) 25 | assert.False(t, store[constants.EnvKeyDisableEmailVerification].(bool)) 26 | assert.False(t, store[constants.EnvKeyDisableMagicLinkLogin].(bool)) 27 | assert.False(t, store[constants.EnvKeyDisableBasicAuthentication].(bool)) 28 | assert.Equal(t, "RS256", store[constants.EnvKeyJwtType].(string)) 29 | assert.Equal(t, store[constants.EnvKeyJwtRoleClaim].(string), "role") 30 | assert.EqualValues(t, store[constants.EnvKeyRoles].(string), "user") 31 | assert.EqualValues(t, store[constants.EnvKeyDefaultRoles].(string), "user") 32 | assert.EqualValues(t, store[constants.EnvKeyAllowedOrigins].(string), "*") 33 | } 34 | -------------------------------------------------------------------------------- /server/test/test_endpoint_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/crypto" 9 | "github.com/authorizerdev/authorizer/server/graph/model" 10 | "github.com/authorizerdev/authorizer/server/memorystore" 11 | "github.com/authorizerdev/authorizer/server/resolvers" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func testEndpointTest(t *testing.T, s TestSetup) { 16 | t.Helper() 17 | t.Run("should test endpoint", func(t *testing.T) { 18 | req, ctx := createContext(s) 19 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 20 | assert.NoError(t, err) 21 | h, err := crypto.EncryptPassword(adminSecret) 22 | assert.NoError(t, err) 23 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 24 | 25 | res, err := resolvers.TestEndpointResolver(ctx, model.TestEndpointRequest{ 26 | Endpoint: s.TestInfo.WebhookEndpoint, 27 | EventName: constants.UserLoginWebhookEvent, 28 | Headers: map[string]interface{}{ 29 | "x-test": "test", 30 | }, 31 | }) 32 | assert.NoError(t, err) 33 | assert.NotNil(t, res) 34 | assert.GreaterOrEqual(t, *res.HTTPStatus, int64(200)) 35 | assert.NotEmpty(t, res.Response) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /server/constants/db_types.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // DbTypePostgres is the postgres database type 5 | DbTypePostgres = "postgres" 6 | // DbTypeSqlite is the sqlite database type 7 | DbTypeSqlite = "sqlite" 8 | // DbTypeLibSQL is the libsql / Turso database type 9 | DbTypeLibSQL = "libsql" 10 | // DbTypeMysql is the mysql database type 11 | DbTypeMysql = "mysql" 12 | // DbTypeSqlserver is the sqlserver database type 13 | DbTypeSqlserver = "sqlserver" 14 | // DbTypeArangodb is the arangodb database type 15 | DbTypeArangodb = "arangodb" 16 | // DbTypeMongodb is the mongodb database type 17 | DbTypeMongodb = "mongodb" 18 | // DbTypeYugabyte is the yugabyte database type 19 | DbTypeYugabyte = "yugabyte" 20 | // DbTypeMariaDB is the mariadb database type 21 | DbTypeMariaDB = "mariadb" 22 | // DbTypeCassandra is the cassandra database type 23 | DbTypeCassandraDB = "cassandradb" 24 | // DbTypeScyllaDB is the scylla database type 25 | DbTypeScyllaDB = "scylladb" 26 | // DbTypeCockroachDB is the cockroach database type 27 | DbTypeCockroachDB = "cockroachdb" 28 | // DbTypePlanetScaleDB is the planetscale database type 29 | DbTypePlanetScaleDB = "planetscale" 30 | // DbTypeDynamoDB is the Dynamo database type 31 | DbTypeDynamoDB = "dynamodb" 32 | // DbTypeCouchbaseDB is the Couchbase database type 33 | DbTypeCouchbaseDB = "couchbase" 34 | ) 35 | -------------------------------------------------------------------------------- /dashboard/src/contexts/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, useContext, useEffect } from 'react'; 2 | import { Center, Spinner } from '@chakra-ui/react'; 3 | import { useQuery } from 'urql'; 4 | import { useLocation, useNavigate } from 'react-router-dom'; 5 | 6 | import { AdminSessionQuery } from '../graphql/queries'; 7 | import { hasAdminSecret } from '../utils'; 8 | 9 | const AuthContext = createContext({ 10 | isLoggedIn: false, 11 | setIsLoggedIn: (data: boolean) => {}, 12 | }); 13 | 14 | export const AuthContextProvider = ({ children }: { children: any }) => { 15 | const [isLoggedIn, setIsLoggedIn] = useState(false); 16 | 17 | const { pathname } = useLocation(); 18 | const navigate = useNavigate(); 19 | 20 | const [{ fetching, data, error }] = useQuery({ 21 | query: AdminSessionQuery, 22 | }); 23 | 24 | useEffect(() => { 25 | if (!fetching && !error) { 26 | setIsLoggedIn(true); 27 | if (pathname === '/login' || pathname === 'signup') { 28 | navigate('/', { replace: true }); 29 | } 30 | } 31 | }, [fetching, error]); 32 | 33 | if (fetching) { 34 | return ( 35 |
36 | 37 |
38 | ); 39 | } 40 | 41 | return ( 42 | 43 | {children} 44 | 45 | ); 46 | }; 47 | 48 | export const useAuthContext = () => useContext(AuthContext); 49 | -------------------------------------------------------------------------------- /dashboard/src/layouts/AuthLayout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Flex, 4 | Image, 5 | Text, 6 | Spinner, 7 | useMediaQuery, 8 | } from '@chakra-ui/react'; 9 | import React from 'react'; 10 | import { useQuery } from 'urql'; 11 | import { MetaQuery } from '../graphql/queries'; 12 | 13 | export function AuthLayout({ children }: { children: React.ReactNode }) { 14 | const [{ fetching, data }] = useQuery({ query: MetaQuery }); 15 | const [isNotSmallerScreen] = useMediaQuery('(min-width:600px)'); 16 | return ( 17 | 25 | 26 | logo 31 | 32 | AUTHORIZER 33 | 34 | 35 | 36 | {fetching ? ( 37 | 38 | ) : ( 39 | <> 40 | 49 | {children} 50 | 51 | 52 | Current Version: {data.meta.version} 53 | 54 | 55 | )} 56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /server/test/forgot_password_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/authorizerdev/authorizer/server/constants" 7 | "github.com/authorizerdev/authorizer/server/db" 8 | "github.com/authorizerdev/authorizer/server/graph/model" 9 | "github.com/authorizerdev/authorizer/server/refs" 10 | "github.com/authorizerdev/authorizer/server/resolvers" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func forgotPasswordTest(t *testing.T, s TestSetup) { 15 | t.Helper() 16 | t.Run(`should run forgot password`, func(t *testing.T) { 17 | _, ctx := createContext(s) 18 | email := "forgot_password." + s.TestInfo.Email 19 | res, err := resolvers.SignupResolver(ctx, model.SignUpInput{ 20 | Email: refs.NewStringRef(email), 21 | Password: s.TestInfo.Password, 22 | ConfirmPassword: s.TestInfo.Password, 23 | }) 24 | assert.NoError(t, err) 25 | assert.NotNil(t, res) 26 | forgotPasswordRes, err := resolvers.ForgotPasswordResolver(ctx, model.ForgotPasswordInput{ 27 | Email: refs.NewStringRef(email), 28 | }) 29 | assert.Nil(t, err, "no errors for forgot password") 30 | assert.NotNil(t, forgotPasswordRes) 31 | verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeForgotPassword) 32 | assert.Nil(t, err) 33 | 34 | assert.Equal(t, verificationRequest.Identifier, constants.VerificationTypeForgotPassword) 35 | 36 | cleanData(email) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /dashboard/src/components/EnvComponents/SessionStorage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Flex, Stack, Center, Text, useMediaQuery } from '@chakra-ui/react'; 3 | import InputField from '../InputField'; 4 | 5 | const SessionStorage = ({ variables, setVariables, RedisURL }: any) => { 6 | const [isNotSmallerScreen] = useMediaQuery('(min-width:600px)'); 7 | return ( 8 |
9 | {' '} 10 | 11 | Session Storage 12 | 13 | 14 | Note: Redis related environment variables cannot be updated from 15 | dashboard. Please use .env file or OS environment variables to update 16 | it. 17 | 18 | 19 | 20 | 21 | Redis URL: 22 | 23 |
27 | 35 |
36 |
37 |
38 |
39 | ); 40 | }; 41 | 42 | export default SessionStorage; 43 | -------------------------------------------------------------------------------- /server/resolvers/delete_webhook.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/authorizerdev/authorizer/server/db" 8 | "github.com/authorizerdev/authorizer/server/graph/model" 9 | "github.com/authorizerdev/authorizer/server/token" 10 | "github.com/authorizerdev/authorizer/server/utils" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // DeleteWebhookResolver resolver to delete webhook and its relevant logs 15 | func DeleteWebhookResolver(ctx context.Context, params model.WebhookRequest) (*model.Response, error) { 16 | gc, err := utils.GinContextFromContext(ctx) 17 | if err != nil { 18 | log.Debug("Failed to get GinContext: ", err) 19 | return nil, err 20 | } 21 | 22 | if !token.IsSuperAdmin(gc) { 23 | log.Debug("Not logged in as super admin") 24 | return nil, fmt.Errorf("unauthorized") 25 | } 26 | 27 | if params.ID == "" { 28 | log.Debug("webhookID is required") 29 | return nil, fmt.Errorf("webhook ID required") 30 | } 31 | 32 | log := log.WithField("webhook_id", params.ID) 33 | 34 | webhook, err := db.Provider.GetWebhookByID(ctx, params.ID) 35 | if err != nil { 36 | log.Debug("failed to get webhook: ", err) 37 | return nil, err 38 | } 39 | 40 | err = db.Provider.DeleteWebhook(ctx, webhook) 41 | if err != nil { 42 | log.Debug("failed to delete webhook: ", err) 43 | return nil, err 44 | } 45 | 46 | return &model.Response{ 47 | Message: "Webhook deleted successfully", 48 | }, nil 49 | } 50 | -------------------------------------------------------------------------------- /scripts/couchbase-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -x 4 | set -m 5 | 6 | sleep 15 7 | 8 | # Setup index and memory quota 9 | # curl -v -X POST http://127.0.0.1:8091/pools/default -d memoryQuota=300 -d indexMemoryQuota=300 10 | 11 | # Setup services 12 | curl -v http://127.0.0.1:8091/node/controller/setupServices -d services=kv%2Cn1ql%2Cindex 13 | 14 | # Setup credentials 15 | curl -v http://127.0.0.1:8091/settings/web -d port=8091 -d username=Administrator -d password=password 16 | 17 | # Setup Memory Optimized Indexes 18 | curl -i -u Administrator:password -X POST http://127.0.0.1:8091/settings/indexes -d 'storageMode=memory_optimized' 19 | 20 | # Load travel-sample bucket 21 | #curl -v -u Administrator:password -X POST http://127.0.0.1:8091/sampleBuckets/install -d '["travel-sample"]' 22 | 23 | echo "Type: $TYPE" 24 | 25 | if [ "$TYPE" = "WORKER" ]; then 26 | echo "Sleeping ..." 27 | sleep 15 28 | 29 | #IP=`hostname -s` 30 | IP=`hostname -I | cut -d ' ' -f1` 31 | echo "IP: " $IP 32 | 33 | echo "Auto Rebalance: $AUTO_REBALANCE" 34 | if [ "$AUTO_REBALANCE" = "true" ]; then 35 | couchbase-cli rebalance --cluster=$COUCHBASE_MASTER:8091 --user=Administrator --password=password --server-add=$IP --server-add-username=Administrator --server-add-password=password 36 | else 37 | couchbase-cli server-add --cluster=$COUCHBASE_MASTER:8091 --user=Administrator --password=password --server-add=$IP --server-add-username=Administrator --server-add-password=password 38 | fi; 39 | fi; -------------------------------------------------------------------------------- /server/resolvers/admin_login.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/authorizerdev/authorizer/server/constants" 10 | "github.com/authorizerdev/authorizer/server/cookie" 11 | "github.com/authorizerdev/authorizer/server/crypto" 12 | "github.com/authorizerdev/authorizer/server/graph/model" 13 | "github.com/authorizerdev/authorizer/server/memorystore" 14 | "github.com/authorizerdev/authorizer/server/utils" 15 | ) 16 | 17 | // AdminLoginResolver is a resolver for admin login mutation 18 | func AdminLoginResolver(ctx context.Context, params model.AdminLoginInput) (*model.Response, error) { 19 | var res *model.Response 20 | 21 | gc, err := utils.GinContextFromContext(ctx) 22 | if err != nil { 23 | log.Debug("Failed to get GinContext: ", err) 24 | return res, err 25 | } 26 | 27 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 28 | if err != nil { 29 | log.Debug("Error getting admin secret: ", err) 30 | return res, err 31 | } 32 | if params.AdminSecret != adminSecret { 33 | log.Debug("Admin secret is not correct") 34 | return res, fmt.Errorf(`invalid admin secret`) 35 | } 36 | 37 | hashedKey, err := crypto.EncryptPassword(adminSecret) 38 | if err != nil { 39 | return res, err 40 | } 41 | cookie.SetAdminCookie(gc, hashedKey) 42 | 43 | res = &model.Response{ 44 | Message: "admin logged in successfully", 45 | } 46 | return res, nil 47 | } 48 | -------------------------------------------------------------------------------- /server/logs/logs.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sirupsen/logrus" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // LogUTCFormatter hels in setting UTC time format for the logs 11 | type LogUTCFormatter struct { 12 | log.Formatter 13 | } 14 | 15 | // Format helps fomratting time to UTC 16 | func (u LogUTCFormatter) Format(e *log.Entry) ([]byte, error) { 17 | e.Time = e.Time.UTC() 18 | return u.Formatter.Format(e) 19 | } 20 | 21 | func InitLog(cliLogLevel string) *log.Logger { 22 | 23 | // log instance for gin server 24 | log := logrus.New() 25 | log.SetFormatter(LogUTCFormatter{&logrus.JSONFormatter{}}) 26 | 27 | if cliLogLevel == "" { 28 | cliLogLevel = os.Getenv("LOG_LEVEL") 29 | } 30 | 31 | var logLevel logrus.Level 32 | switch cliLogLevel { 33 | case "debug": 34 | logLevel = logrus.DebugLevel 35 | case "info": 36 | logLevel = logrus.InfoLevel 37 | case "warn": 38 | logLevel = logrus.WarnLevel 39 | case "error": 40 | logLevel = logrus.ErrorLevel 41 | case "fatal": 42 | logLevel = logrus.FatalLevel 43 | case "panic": 44 | logLevel = logrus.PanicLevel 45 | default: 46 | logLevel = logrus.InfoLevel 47 | } 48 | // set log level globally 49 | logrus.SetLevel(logLevel) 50 | 51 | // set log level for go-gin middleware 52 | log.SetLevel(logLevel) 53 | 54 | // show file path in log for debug or other log levels. 55 | if logLevel != logrus.InfoLevel { 56 | logrus.SetReportCaller(true) 57 | log.SetReportCaller(true) 58 | } 59 | 60 | return log 61 | } 62 | -------------------------------------------------------------------------------- /server/handlers/playground.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/99designs/gqlgen/graphql/playground" 7 | "github.com/gin-gonic/gin" 8 | 9 | log "github.com/sirupsen/logrus" 10 | 11 | "github.com/authorizerdev/authorizer/server/constants" 12 | "github.com/authorizerdev/authorizer/server/memorystore" 13 | "github.com/authorizerdev/authorizer/server/token" 14 | ) 15 | 16 | // PlaygroundHandler is the handler for the /playground route 17 | func PlaygroundHandler() gin.HandlerFunc { 18 | return func(c *gin.Context) { 19 | var h http.HandlerFunc 20 | 21 | disablePlayground, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisablePlayGround) 22 | if err != nil { 23 | log.Debug("error while getting disable playground value") 24 | disablePlayground = false 25 | } 26 | 27 | // if env set to false, then check if logged in as super admin, if logged in then return graphql else 401 error 28 | // if env set to true, then disabled the playground with 404 error 29 | if !disablePlayground { 30 | if token.IsSuperAdmin(c) { 31 | h = playground.Handler("GraphQL", "/graphql") 32 | } else { 33 | log.Debug("not logged in as super admin") 34 | c.JSON(http.StatusUnauthorized, gin.H{"error": "not logged in as super admin"}) 35 | return 36 | } 37 | } else { 38 | log.Debug("playground is disabled") 39 | c.JSON(http.StatusNotFound, gin.H{"error": "playground is disabled"}) 40 | return 41 | } 42 | h.ServeHTTP(c.Writer, c.Request) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /server/resolvers/webhook_logs.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/authorizerdev/authorizer/server/db" 8 | "github.com/authorizerdev/authorizer/server/graph/model" 9 | "github.com/authorizerdev/authorizer/server/refs" 10 | "github.com/authorizerdev/authorizer/server/token" 11 | "github.com/authorizerdev/authorizer/server/utils" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // WebhookLogsResolver resolver for getting the list of webhook_logs based on pagination & webhook identifier 16 | func WebhookLogsResolver(ctx context.Context, params *model.ListWebhookLogRequest) (*model.WebhookLogs, error) { 17 | gc, err := utils.GinContextFromContext(ctx) 18 | if err != nil { 19 | log.Debug("Failed to get GinContext: ", err) 20 | return nil, err 21 | } 22 | 23 | if !token.IsSuperAdmin(gc) { 24 | log.Debug("Not logged in as super admin") 25 | return nil, fmt.Errorf("unauthorized") 26 | } 27 | 28 | var pagination *model.Pagination 29 | var webhookID string 30 | 31 | if params != nil { 32 | pagination = utils.GetPagination(&model.PaginatedInput{ 33 | Pagination: params.Pagination, 34 | }) 35 | webhookID = refs.StringValue(params.WebhookID) 36 | } else { 37 | pagination = utils.GetPagination(nil) 38 | webhookID = "" 39 | } 40 | // TODO fix 41 | webhookLogs, err := db.Provider.ListWebhookLogs(ctx, pagination, webhookID) 42 | if err != nil { 43 | log.Debug("failed to get webhook logs: ", err) 44 | return nil, err 45 | } 46 | return webhookLogs, nil 47 | } 48 | -------------------------------------------------------------------------------- /server/test/delete_user_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/crypto" 9 | "github.com/authorizerdev/authorizer/server/graph/model" 10 | "github.com/authorizerdev/authorizer/server/memorystore" 11 | "github.com/authorizerdev/authorizer/server/refs" 12 | "github.com/authorizerdev/authorizer/server/resolvers" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func deleteUserTest(t *testing.T, s TestSetup) { 17 | t.Helper() 18 | t.Run(`should delete users with admin secret only`, func(t *testing.T) { 19 | req, ctx := createContext(s) 20 | email := "delete_user." + s.TestInfo.Email 21 | resolvers.SignupResolver(ctx, model.SignUpInput{ 22 | Email: refs.NewStringRef(email), 23 | Password: s.TestInfo.Password, 24 | ConfirmPassword: s.TestInfo.Password, 25 | }) 26 | 27 | _, err := resolvers.DeleteUserResolver(ctx, model.DeleteUserInput{ 28 | Email: email, 29 | }) 30 | assert.NotNil(t, err, "unauthorized") 31 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 32 | assert.Nil(t, err) 33 | 34 | h, err := crypto.EncryptPassword(adminSecret) 35 | assert.Nil(t, err) 36 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 37 | 38 | _, err = resolvers.DeleteUserResolver(ctx, model.DeleteUserInput{ 39 | Email: email, 40 | }) 41 | assert.Nil(t, err) 42 | cleanData(email) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /server/db/providers/couchbase/shared.go: -------------------------------------------------------------------------------- 1 | package couchbase 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/couchbase/gocb/v2" 10 | ) 11 | 12 | func GetSetFields(webhookMap map[string]interface{}) (string, map[string]interface{}) { 13 | params := make(map[string]interface{}, 1) 14 | updateFields := "" 15 | for key, value := range webhookMap { 16 | if key == "_id" { 17 | continue 18 | } 19 | if key == "_key" { 20 | continue 21 | } 22 | if value == nil { 23 | updateFields += fmt.Sprintf("%s=$%s,", key, key) 24 | params[key] = "null" 25 | continue 26 | } 27 | valueType := reflect.TypeOf(value) 28 | if valueType.Name() == "string" { 29 | updateFields += fmt.Sprintf("%s = $%s, ", key, key) 30 | params[key] = value.(string) 31 | 32 | } else { 33 | updateFields += fmt.Sprintf("%s = $%s, ", key, key) 34 | params[key] = value 35 | } 36 | } 37 | updateFields = strings.Trim(updateFields, " ") 38 | updateFields = strings.TrimSuffix(updateFields, ",") 39 | return updateFields, params 40 | } 41 | 42 | func (p *provider) GetTotalDocs(ctx context.Context, collection string) (int64, error) { 43 | totalDocs := TotalDocs{} 44 | countQuery := fmt.Sprintf("SELECT COUNT(*) as Total FROM %s.%s", p.scopeName, collection) 45 | queryRes, err := p.db.Query(countQuery, &gocb.QueryOptions{ 46 | Context: ctx, 47 | }) 48 | queryRes.One(&totalDocs) 49 | if err != nil { 50 | return 0, err 51 | } 52 | return totalDocs.Total, nil 53 | } 54 | 55 | type TotalDocs struct { 56 | Total int64 57 | } 58 | -------------------------------------------------------------------------------- /server/resolvers/delete_email_template.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/authorizerdev/authorizer/server/db" 8 | "github.com/authorizerdev/authorizer/server/graph/model" 9 | "github.com/authorizerdev/authorizer/server/token" 10 | "github.com/authorizerdev/authorizer/server/utils" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // DeleteEmailTemplateResolver resolver to delete email template and its relevant logs 15 | func DeleteEmailTemplateResolver(ctx context.Context, params model.DeleteEmailTemplateRequest) (*model.Response, error) { 16 | gc, err := utils.GinContextFromContext(ctx) 17 | if err != nil { 18 | log.Debug("Failed to get GinContext: ", err) 19 | return nil, err 20 | } 21 | 22 | if !token.IsSuperAdmin(gc) { 23 | log.Debug("Not logged in as super admin") 24 | return nil, fmt.Errorf("unauthorized") 25 | } 26 | 27 | if params.ID == "" { 28 | log.Debug("email template is required") 29 | return nil, fmt.Errorf("email template ID required") 30 | } 31 | 32 | log := log.WithField("email_template_id", params.ID) 33 | 34 | emailTemplate, err := db.Provider.GetEmailTemplateByID(ctx, params.ID) 35 | if err != nil { 36 | log.Debug("failed to get email template: ", err) 37 | return nil, err 38 | } 39 | 40 | err = db.Provider.DeleteEmailTemplate(ctx, emailTemplate) 41 | if err != nil { 42 | log.Debug("failed to delete email template: ", err) 43 | return nil, err 44 | } 45 | 46 | return &model.Response{ 47 | Message: "Email templated deleted successfully", 48 | }, nil 49 | } 50 | -------------------------------------------------------------------------------- /server/resolvers/admin_session.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/authorizerdev/authorizer/server/constants" 10 | "github.com/authorizerdev/authorizer/server/cookie" 11 | "github.com/authorizerdev/authorizer/server/crypto" 12 | "github.com/authorizerdev/authorizer/server/graph/model" 13 | "github.com/authorizerdev/authorizer/server/memorystore" 14 | "github.com/authorizerdev/authorizer/server/token" 15 | "github.com/authorizerdev/authorizer/server/utils" 16 | ) 17 | 18 | // AdminSessionResolver is a resolver for admin session query 19 | func AdminSessionResolver(ctx context.Context) (*model.Response, error) { 20 | var res *model.Response 21 | 22 | gc, err := utils.GinContextFromContext(ctx) 23 | if err != nil { 24 | log.Debug("Failed to get GinContext: ", err) 25 | return res, err 26 | } 27 | 28 | if !token.IsSuperAdmin(gc) { 29 | log.Debug("Not logged in as super admin") 30 | return res, fmt.Errorf("unauthorized") 31 | } 32 | 33 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 34 | if err != nil { 35 | log.Debug("Error getting admin secret: ", err) 36 | return res, fmt.Errorf("unauthorized") 37 | } 38 | hashedKey, err := crypto.EncryptPassword(adminSecret) 39 | if err != nil { 40 | log.Debug("Failed to encrypt key: ", err) 41 | return res, err 42 | } 43 | cookie.SetAdminCookie(gc, hashedKey) 44 | 45 | res = &model.Response{ 46 | Message: "admin logged in successfully", 47 | } 48 | return res, nil 49 | } 50 | -------------------------------------------------------------------------------- /server/constants/auth_methods.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // AuthRecipeMethodBasicAuth is the basic_auth auth method 5 | AuthRecipeMethodBasicAuth = "basic_auth" 6 | // AuthRecipeMethodMobileBasicAuth is the mobile basic_auth method, where user can signup using mobile number and password 7 | AuthRecipeMethodMobileBasicAuth = "mobile_basic_auth" 8 | // AuthRecipeMethodMagicLinkLogin is the magic_link_login auth method 9 | AuthRecipeMethodMagicLinkLogin = "magic_link_login" 10 | // AuthRecipeMethodMobileOTP is the mobile_otp auth method 11 | AuthRecipeMethodMobileOTP = "mobile_otp" 12 | // AuthRecipeMethodGoogle is the google auth method 13 | AuthRecipeMethodGoogle = "google" 14 | // AuthRecipeMethodGithub is the github auth method 15 | AuthRecipeMethodGithub = "github" 16 | // AuthRecipeMethodFacebook is the facebook auth method 17 | AuthRecipeMethodFacebook = "facebook" 18 | // AuthRecipeMethodLinkedin is the linkedin auth method 19 | AuthRecipeMethodLinkedIn = "linkedin" 20 | // AuthRecipeMethodApple is the apple auth method 21 | AuthRecipeMethodApple = "apple" 22 | // AuthRecipeMethodDiscord is the discord auth method 23 | AuthRecipeMethodDiscord = "discord" 24 | // AuthRecipeMethodTwitter is the twitter auth method 25 | AuthRecipeMethodTwitter = "twitter" 26 | // AuthRecipeMethodMicrosoft is the microsoft auth method 27 | AuthRecipeMethodMicrosoft = "microsoft" 28 | // AuthRecipeMethodTwitch is the twitch auth method 29 | AuthRecipeMethodTwitch = "twitch" 30 | // AuthRecipeMethodRoblox is the roblox auth method 31 | AuthRecipeMethodRoblox = "roblox" 32 | ) 33 | -------------------------------------------------------------------------------- /server/validators/url.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/memorystore" 9 | "github.com/authorizerdev/authorizer/server/parsers" 10 | ) 11 | 12 | // IsValidOrigin validates origin based on ALLOWED_ORIGINS 13 | func IsValidOrigin(url string) bool { 14 | allowedOriginsString, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAllowedOrigins) 15 | allowedOrigins := []string{} 16 | if err != nil { 17 | allowedOrigins = []string{"*"} 18 | } else { 19 | allowedOrigins = strings.Split(allowedOriginsString, ",") 20 | } 21 | if len(allowedOrigins) == 1 && allowedOrigins[0] == "*" { 22 | return true 23 | } 24 | 25 | hasValidURL := false 26 | hostName, port := parsers.GetHostParts(url) 27 | currentOrigin := hostName + ":" + port 28 | 29 | for _, origin := range allowedOrigins { 30 | replacedString := origin 31 | // if has regex whitelisted domains 32 | if strings.Contains(origin, "*") { 33 | replacedString = strings.ReplaceAll(origin, ".", "\\.") 34 | replacedString = strings.ReplaceAll(replacedString, "*", ".*") 35 | 36 | if strings.HasPrefix(replacedString, ".*") { 37 | replacedString += "\\b" 38 | } 39 | 40 | if strings.HasSuffix(replacedString, ".*") { 41 | replacedString = "\\b" + replacedString 42 | } 43 | } 44 | 45 | if matched, _ := regexp.MatchString(replacedString, currentOrigin); matched { 46 | hasValidURL = true 47 | break 48 | } 49 | } 50 | 51 | return hasValidURL 52 | } 53 | -------------------------------------------------------------------------------- /server/resolvers/user.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | log "github.com/sirupsen/logrus" 9 | 10 | "github.com/authorizerdev/authorizer/server/db" 11 | "github.com/authorizerdev/authorizer/server/graph/model" 12 | "github.com/authorizerdev/authorizer/server/token" 13 | "github.com/authorizerdev/authorizer/server/utils" 14 | ) 15 | 16 | // UserResolver is a resolver for user query 17 | // This is admin only query 18 | func UserResolver(ctx context.Context, params model.GetUserRequest) (*model.User, error) { 19 | gc, err := utils.GinContextFromContext(ctx) 20 | if err != nil { 21 | log.Debug("Failed to get GinContext: ", err) 22 | return nil, err 23 | } 24 | if !token.IsSuperAdmin(gc) { 25 | log.Debug("Not logged in as super admin.") 26 | return nil, fmt.Errorf("unauthorized") 27 | } 28 | // Try getting user by ID 29 | if params.ID != nil && strings.Trim(*params.ID, " ") != "" { 30 | res, err := db.Provider.GetUserByID(ctx, *params.ID) 31 | if err != nil { 32 | log.Debug("Failed to get users by ID: ", err) 33 | return nil, err 34 | } 35 | return res.AsAPIUser(), nil 36 | } 37 | // Try getting user by email 38 | if params.Email != nil && strings.Trim(*params.Email, " ") != "" { 39 | res, err := db.Provider.GetUserByEmail(ctx, *params.Email) 40 | if err != nil { 41 | log.Debug("Failed to get users by email: ", err) 42 | return nil, err 43 | } 44 | return res.AsAPIUser(), nil 45 | } 46 | // Return error if no params are provided 47 | return nil, fmt.Errorf("invalid params, user id or email is required") 48 | } 49 | -------------------------------------------------------------------------------- /server/resolvers/enable_access.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/authorizerdev/authorizer/server/constants" 10 | "github.com/authorizerdev/authorizer/server/db" 11 | "github.com/authorizerdev/authorizer/server/graph/model" 12 | "github.com/authorizerdev/authorizer/server/token" 13 | "github.com/authorizerdev/authorizer/server/utils" 14 | ) 15 | 16 | // EnableAccessResolver is a resolver for enabling user access 17 | func EnableAccessResolver(ctx context.Context, params model.UpdateAccessInput) (*model.Response, error) { 18 | var res *model.Response 19 | 20 | gc, err := utils.GinContextFromContext(ctx) 21 | if err != nil { 22 | log.Debug("Failed to get GinContext: ", err) 23 | return res, err 24 | } 25 | 26 | if !token.IsSuperAdmin(gc) { 27 | log.Debug("Not logged in as super admin.") 28 | return res, fmt.Errorf("unauthorized") 29 | } 30 | 31 | log := log.WithFields(log.Fields{ 32 | "user_id": params.UserID, 33 | }) 34 | 35 | user, err := db.Provider.GetUserByID(ctx, params.UserID) 36 | if err != nil { 37 | log.Debug("Failed to get user from DB: ", err) 38 | return res, err 39 | } 40 | 41 | user.RevokedTimestamp = nil 42 | 43 | user, err = db.Provider.UpdateUser(ctx, user) 44 | if err != nil { 45 | log.Debug("Failed to update user: ", err) 46 | return res, err 47 | } 48 | 49 | res = &model.Response{ 50 | Message: `user access enabled successfully`, 51 | } 52 | 53 | go utils.RegisterEvent(ctx, constants.UserAccessEnabledWebhookEvent, "", user) 54 | 55 | return res, nil 56 | } 57 | -------------------------------------------------------------------------------- /server/db/models/email_templates.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/authorizerdev/authorizer/server/graph/model" 7 | "github.com/authorizerdev/authorizer/server/refs" 8 | ) 9 | 10 | // EmailTemplate model for database 11 | type EmailTemplate struct { 12 | Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty" dynamo:"key,omitempty"` // for arangodb 13 | ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id" dynamo:"id,hash"` 14 | EventName string `gorm:"unique" json:"event_name" bson:"event_name" cql:"event_name" dynamo:"event_name" index:"event_name,hash"` 15 | Subject string `json:"subject" bson:"subject" cql:"subject" dynamo:"subject"` 16 | Template string `json:"template" bson:"template" cql:"template" dynamo:"template"` 17 | Design string `json:"design" bson:"design" cql:"design" dynamo:"design"` 18 | CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at" dynamo:"created_at"` 19 | UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at" dynamo:"updated_at"` 20 | } 21 | 22 | // AsAPIEmailTemplate to return email template as graphql response object 23 | func (e *EmailTemplate) AsAPIEmailTemplate() *model.EmailTemplate { 24 | id := e.ID 25 | if strings.Contains(id, Collections.EmailTemplate+"/") { 26 | id = strings.TrimPrefix(id, Collections.EmailTemplate+"/") 27 | } 28 | return &model.EmailTemplate{ 29 | ID: id, 30 | EventName: e.EventName, 31 | Subject: e.Subject, 32 | Template: e.Template, 33 | Design: e.Design, 34 | CreatedAt: refs.NewInt64Ref(e.CreatedAt), 35 | UpdatedAt: refs.NewInt64Ref(e.UpdatedAt), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /dashboard/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from 'react'; 2 | import { Outlet, Route, Routes } from 'react-router-dom'; 3 | 4 | import { useAuthContext } from '../contexts/AuthContext'; 5 | import { DashboardLayout } from '../layouts/DashboardLayout'; 6 | import EmailTemplates from '../pages/EmailTemplates'; 7 | 8 | const Auth = lazy(() => import('../pages/Auth')); 9 | const Environment = lazy(() => import('../pages/Environment')); 10 | const Home = lazy(() => import('../pages/Home')); 11 | const Users = lazy(() => import('../pages/Users')); 12 | const Webhooks = lazy(() => import('../pages/Webhooks')); 13 | 14 | export const AppRoutes = () => { 15 | const { isLoggedIn } = useAuthContext(); 16 | 17 | if (isLoggedIn) { 18 | return ( 19 |
20 | }> 21 | 22 | 25 | 26 | 27 | } 28 | > 29 | }> 30 | } /> 31 | } /> 32 | 33 | } /> 34 | } /> 35 | } /> 36 | } /> 37 | 38 | 39 | 40 |
41 | ); 42 | } 43 | return ( 44 | }> 45 | 46 | } /> 47 | } /> 48 | 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /server/validators/password.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/authorizerdev/authorizer/server/constants" 7 | "github.com/authorizerdev/authorizer/server/memorystore" 8 | ) 9 | 10 | // ValidatePassword to validate the password against the following policy 11 | // min char length: 6 12 | // max char length: 36 13 | // at least one upper case letter 14 | // at least one lower case letter 15 | // at least one digit 16 | // at least one special character 17 | func IsValidPassword(password string) error { 18 | if len(password) < 6 || len(password) > 36 { 19 | return errors.New("password must be of minimum 6 characters and maximum 36 characters") 20 | } 21 | 22 | // if strong password is disabled 23 | // just check for min 6 chars & max 36 24 | isStrongPasswordDisabled, _ := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableStrongPassword) 25 | if isStrongPasswordDisabled { 26 | return nil 27 | } 28 | 29 | hasUpperCase := false 30 | hasLowerCase := false 31 | hasDigit := false 32 | hasSpecialChar := false 33 | 34 | for _, char := range password { 35 | if char >= 'A' && char <= 'Z' { 36 | hasUpperCase = true 37 | } else if char >= 'a' && char <= 'z' { 38 | hasLowerCase = true 39 | } else if char >= '0' && char <= '9' { 40 | hasDigit = true 41 | } else { 42 | hasSpecialChar = true 43 | } 44 | } 45 | 46 | isValid := hasUpperCase && hasLowerCase && hasDigit && hasSpecialChar 47 | 48 | if isValid { 49 | return nil 50 | } 51 | 52 | return errors.New(`password is not valid. It needs to be at least 6 characters long and contain at least one number, one uppercase letter, one lowercase letter and one special character`) 53 | } 54 | -------------------------------------------------------------------------------- /server/db/providers/dynamodb/env.go: -------------------------------------------------------------------------------- 1 | package dynamodb 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/authorizerdev/authorizer/server/db/models" 10 | "github.com/google/uuid" 11 | ) 12 | 13 | // AddEnv to save environment information in database 14 | func (p *provider) AddEnv(ctx context.Context, env *models.Env) (*models.Env, error) { 15 | collection := p.db.Table(models.Collections.Env) 16 | if env.ID == "" { 17 | env.ID = uuid.New().String() 18 | } 19 | env.Key = env.ID 20 | env.CreatedAt = time.Now().Unix() 21 | env.UpdatedAt = time.Now().Unix() 22 | err := collection.Put(env).RunWithContext(ctx) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return env, nil 27 | } 28 | 29 | // UpdateEnv to update environment information in database 30 | func (p *provider) UpdateEnv(ctx context.Context, env *models.Env) (*models.Env, error) { 31 | collection := p.db.Table(models.Collections.Env) 32 | env.UpdatedAt = time.Now().Unix() 33 | err := UpdateByHashKey(collection, "id", env.ID, env) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return env, nil 38 | } 39 | 40 | // GetEnv to get environment information from database 41 | func (p *provider) GetEnv(ctx context.Context) (*models.Env, error) { 42 | var env *models.Env 43 | collection := p.db.Table(models.Collections.Env) 44 | // As there is no Findone supported. 45 | iter := collection.Scan().Limit(1).Iter() 46 | for iter.NextWithContext(ctx, &env) { 47 | if env == nil { 48 | return nil, errors.New("no documets found") 49 | } else { 50 | return env, nil 51 | } 52 | } 53 | err := iter.Err() 54 | if err != nil { 55 | return env, fmt.Errorf("config not found") 56 | } 57 | return env, nil 58 | } 59 | -------------------------------------------------------------------------------- /server/test/delete_email_template_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/crypto" 9 | "github.com/authorizerdev/authorizer/server/db" 10 | "github.com/authorizerdev/authorizer/server/graph/model" 11 | "github.com/authorizerdev/authorizer/server/memorystore" 12 | "github.com/authorizerdev/authorizer/server/resolvers" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func deleteEmailTemplateTest(t *testing.T, s TestSetup) { 17 | t.Helper() 18 | t.Run("should delete email templates", func(t *testing.T) { 19 | req, ctx := createContext(s) 20 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 21 | assert.NoError(t, err) 22 | h, err := crypto.EncryptPassword(adminSecret) 23 | assert.NoError(t, err) 24 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 25 | 26 | // get all email templates 27 | emailTemplates, err := db.Provider.ListEmailTemplate(ctx, &model.Pagination{ 28 | Limit: 10, 29 | Page: 1, 30 | Offset: 0, 31 | }) 32 | assert.NoError(t, err) 33 | 34 | for _, e := range emailTemplates.EmailTemplates { 35 | res, err := resolvers.DeleteEmailTemplateResolver(ctx, model.DeleteEmailTemplateRequest{ 36 | ID: e.ID, 37 | }) 38 | 39 | assert.NoError(t, err) 40 | assert.NotNil(t, res) 41 | assert.NotEmpty(t, res.Message) 42 | } 43 | 44 | emailTemplates, err = db.Provider.ListEmailTemplate(ctx, &model.Pagination{ 45 | Limit: 10, 46 | Page: 1, 47 | Offset: 0, 48 | }) 49 | assert.NoError(t, err) 50 | assert.Len(t, emailTemplates.EmailTemplates, 0) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /server/test/verify_email_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/authorizerdev/authorizer/server/constants" 7 | "github.com/authorizerdev/authorizer/server/db" 8 | "github.com/authorizerdev/authorizer/server/graph/model" 9 | "github.com/authorizerdev/authorizer/server/refs" 10 | "github.com/authorizerdev/authorizer/server/resolvers" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func verifyEmailTest(t *testing.T, s TestSetup) { 15 | t.Helper() 16 | t.Run(`should verify email`, func(t *testing.T) { 17 | _, ctx := createContext(s) 18 | email := "verify_email." + s.TestInfo.Email 19 | res, err := resolvers.SignupResolver(ctx, model.SignUpInput{ 20 | Email: refs.NewStringRef(email), 21 | Password: s.TestInfo.Password, 22 | ConfirmPassword: s.TestInfo.Password, 23 | }) 24 | assert.NoError(t, err) 25 | assert.NotNil(t, res) 26 | user := *res.User 27 | assert.Equal(t, email, refs.StringValue(user.Email)) 28 | assert.Nil(t, res.AccessToken, "access token should be nil") 29 | verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeBasicAuthSignup) 30 | assert.Nil(t, err) 31 | assert.Equal(t, email, verificationRequest.Email) 32 | 33 | verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{ 34 | Token: verificationRequest.Token, 35 | }) 36 | assert.Nil(t, err) 37 | assert.NotEqual(t, verifyRes.AccessToken, "", "access token should not be empty") 38 | // Check if phone number is verified 39 | user1, err := db.Provider.GetUserByEmail(ctx, email) 40 | assert.NoError(t, err) 41 | assert.NotNil(t, user1) 42 | assert.NotNil(t, user1.EmailVerifiedAt) 43 | cleanData(email) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /server/test/users_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/crypto" 9 | "github.com/authorizerdev/authorizer/server/graph/model" 10 | "github.com/authorizerdev/authorizer/server/memorystore" 11 | "github.com/authorizerdev/authorizer/server/refs" 12 | "github.com/authorizerdev/authorizer/server/resolvers" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func usersTest(t *testing.T, s TestSetup) { 17 | t.Helper() 18 | t.Run(`should get users list with admin secret only`, func(t *testing.T) { 19 | req, ctx := createContext(s) 20 | email := "users." + s.TestInfo.Email 21 | resolvers.SignupResolver(ctx, model.SignUpInput{ 22 | Email: refs.NewStringRef(email), 23 | Password: s.TestInfo.Password, 24 | ConfirmPassword: s.TestInfo.Password, 25 | }) 26 | 27 | limit := int64(10) 28 | page := int64(1) 29 | pagination := &model.PaginatedInput{ 30 | Pagination: &model.PaginationInput{ 31 | Limit: &limit, 32 | Page: &page, 33 | }, 34 | } 35 | 36 | usersRes, err := resolvers.UsersResolver(ctx, pagination) 37 | assert.NotNil(t, err, "unauthorized") 38 | assert.Nil(t, usersRes) 39 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 40 | assert.Nil(t, err) 41 | h, err := crypto.EncryptPassword(adminSecret) 42 | assert.Nil(t, err) 43 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 44 | 45 | usersRes, err = resolvers.UsersResolver(ctx, pagination) 46 | assert.Nil(t, err) 47 | rLen := len(usersRes.Users) 48 | assert.GreaterOrEqual(t, rLen, 1) 49 | 50 | cleanData(email) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /server/resolvers/deactivate_account.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/db" 9 | "github.com/authorizerdev/authorizer/server/graph/model" 10 | "github.com/authorizerdev/authorizer/server/memorystore" 11 | "github.com/authorizerdev/authorizer/server/token" 12 | "github.com/authorizerdev/authorizer/server/utils" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // DeactivateAccountResolver is the resolver for the deactivate_account field. 17 | func DeactivateAccountResolver(ctx context.Context) (*model.Response, error) { 18 | var res *model.Response 19 | gc, err := utils.GinContextFromContext(ctx) 20 | if err != nil { 21 | log.Debug("Failed to get GinContext: ", err) 22 | return res, err 23 | } 24 | tokenData, err := token.GetUserIDFromSessionOrAccessToken(gc) 25 | if err != nil { 26 | log.Debug("Failed GetUserIDFromSessionOrAccessToken: ", err) 27 | return res, err 28 | } 29 | log := log.WithFields(log.Fields{ 30 | "user_id": tokenData.UserID, 31 | }) 32 | user, err := db.Provider.GetUserByID(ctx, tokenData.UserID) 33 | if err != nil { 34 | log.Debug("Failed to get user by id: ", err) 35 | return res, err 36 | } 37 | now := time.Now().Unix() 38 | user.RevokedTimestamp = &now 39 | user, err = db.Provider.UpdateUser(ctx, user) 40 | if err != nil { 41 | log.Debug("Failed to update user: ", err) 42 | return res, err 43 | } 44 | go func() { 45 | memorystore.Provider.DeleteAllUserSessions(user.ID) 46 | utils.RegisterEvent(ctx, constants.UserDeactivatedWebhookEvent, "", user) 47 | }() 48 | res = &model.Response{ 49 | Message: `user account deactivated successfully`, 50 | } 51 | return res, nil 52 | } 53 | -------------------------------------------------------------------------------- /server/resolvers/revoke_access.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | log "github.com/sirupsen/logrus" 9 | 10 | "github.com/authorizerdev/authorizer/server/constants" 11 | "github.com/authorizerdev/authorizer/server/db" 12 | "github.com/authorizerdev/authorizer/server/graph/model" 13 | "github.com/authorizerdev/authorizer/server/memorystore" 14 | "github.com/authorizerdev/authorizer/server/token" 15 | "github.com/authorizerdev/authorizer/server/utils" 16 | ) 17 | 18 | // RevokeAccessResolver is a resolver for revoking user access 19 | func RevokeAccessResolver(ctx context.Context, params model.UpdateAccessInput) (*model.Response, error) { 20 | var res *model.Response 21 | 22 | gc, err := utils.GinContextFromContext(ctx) 23 | if err != nil { 24 | log.Debug("Failed to get GinContext: ", err) 25 | return res, err 26 | } 27 | 28 | if !token.IsSuperAdmin(gc) { 29 | log.Debug("Not logged in as super admin") 30 | return res, fmt.Errorf("unauthorized") 31 | } 32 | 33 | log := log.WithFields(log.Fields{ 34 | "user_id": params.UserID, 35 | }) 36 | user, err := db.Provider.GetUserByID(ctx, params.UserID) 37 | if err != nil { 38 | log.Debug("Failed to get user by ID: ", err) 39 | return res, err 40 | } 41 | 42 | now := time.Now().Unix() 43 | user.RevokedTimestamp = &now 44 | 45 | user, err = db.Provider.UpdateUser(ctx, user) 46 | if err != nil { 47 | log.Debug("Failed to update user: ", err) 48 | return res, err 49 | } 50 | 51 | go func() { 52 | memorystore.Provider.DeleteAllUserSessions(user.ID) 53 | utils.RegisterEvent(ctx, constants.UserAccessRevokedWebhookEvent, "", user) 54 | }() 55 | 56 | res = &model.Response{ 57 | Message: `user access revoked successfully`, 58 | } 59 | 60 | return res, nil 61 | } 62 | -------------------------------------------------------------------------------- /server/db/providers/sql/authenticator.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | "gorm.io/gorm/clause" 9 | 10 | "github.com/authorizerdev/authorizer/server/db/models" 11 | ) 12 | 13 | func (p *provider) AddAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) { 14 | exists, _ := p.GetAuthenticatorDetailsByUserId(ctx, authenticators.UserID, authenticators.Method) 15 | if exists != nil { 16 | return authenticators, nil 17 | } 18 | 19 | if authenticators.ID == "" { 20 | authenticators.ID = uuid.New().String() 21 | } 22 | authenticators.Key = authenticators.ID 23 | authenticators.CreatedAt = time.Now().Unix() 24 | authenticators.UpdatedAt = time.Now().Unix() 25 | res := p.db.Clauses( 26 | clause.OnConflict{ 27 | UpdateAll: true, 28 | Columns: []clause.Column{{Name: "id"}}, 29 | }).Create(&authenticators) 30 | if res.Error != nil { 31 | return nil, res.Error 32 | } 33 | return authenticators, nil 34 | } 35 | 36 | func (p *provider) UpdateAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) { 37 | authenticators.UpdatedAt = time.Now().Unix() 38 | result := p.db.Save(&authenticators) 39 | if result.Error != nil { 40 | return authenticators, result.Error 41 | } 42 | return authenticators, nil 43 | } 44 | 45 | func (p *provider) GetAuthenticatorDetailsByUserId(ctx context.Context, userId string, authenticatorType string) (*models.Authenticator, error) { 46 | var authenticators models.Authenticator 47 | result := p.db.Where("user_id = ?", userId).Where("method = ?", authenticatorType).First(&authenticators) 48 | if result.Error != nil { 49 | return nil, result.Error 50 | } 51 | return &authenticators, nil 52 | } 53 | -------------------------------------------------------------------------------- /server/constants/oauth_info_urls.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // Ref: https://github.com/qor/auth/blob/master/providers/google/google.go 5 | // deprecated and not used. instead we follow open id approach for google login 6 | GoogleUserInfoURL = "https://www.googleapis.com/oauth2/v3/userinfo" 7 | // Ref: https://github.com/qor/auth/blob/master/providers/facebook/facebook.go#L18 8 | FacebookUserInfoURL = "https://graph.facebook.com/me?fields=id,first_name,last_name,name,email,picture&access_token=" 9 | // Ref: https://docs.github.com/en/developers/apps/building-github-apps/identifying-and-authorizing-users-for-github-apps#3-your-github-app-accesses-the-api-with-the-users-access-token 10 | GithubUserInfoURL = "https://api.github.com/user" 11 | // Get github user emails when user info email is empty Ref: https://stackoverflow.com/a/35387123 12 | GithubUserEmails = "https://api.github.com/user/emails" 13 | 14 | // Ref: https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api 15 | LinkedInUserInfoURL = "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,emailAddress,profilePicture(displayImage~:playableStreams))" 16 | LinkedInEmailURL = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))" 17 | 18 | TwitterUserInfoURL = "https://api.twitter.com/2/users/me?user.fields=id,name,profile_image_url,username" 19 | 20 | // RobloxUserInfoURL is the URL to get user info from Roblox 21 | RobloxUserInfoURL = "https://apis.roblox.com/oauth/v1/userinfo" 22 | 23 | DiscordUserInfoURL = "https://discord.com/api/oauth2/@me" 24 | // Get microsoft user info. 25 | // Ref: https://learn.microsoft.com/en-us/azure/active-directory/develop/userinfo 26 | MicrosoftUserInfoURL = "https://graph.microsoft.com/oidc/userinfo" 27 | ) 28 | -------------------------------------------------------------------------------- /server/test/webhook_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/crypto" 9 | "github.com/authorizerdev/authorizer/server/db" 10 | "github.com/authorizerdev/authorizer/server/graph/model" 11 | "github.com/authorizerdev/authorizer/server/memorystore" 12 | "github.com/authorizerdev/authorizer/server/refs" 13 | "github.com/authorizerdev/authorizer/server/resolvers" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func webhookTest(t *testing.T, s TestSetup) { 18 | t.Helper() 19 | t.Run("should get webhook", func(t *testing.T) { 20 | req, ctx := createContext(s) 21 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 22 | assert.NoError(t, err) 23 | h, err := crypto.EncryptPassword(adminSecret) 24 | assert.NoError(t, err) 25 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 26 | 27 | // get webhook by event name 28 | webhooks, err := db.Provider.GetWebhookByEventName(ctx, constants.UserCreatedWebhookEvent) 29 | assert.NoError(t, err) 30 | assert.NotNil(t, webhooks) 31 | assert.GreaterOrEqual(t, len(webhooks), 2) 32 | for _, webhook := range webhooks { 33 | res, err := resolvers.WebhookResolver(ctx, model.WebhookRequest{ 34 | ID: webhook.ID, 35 | }) 36 | assert.NoError(t, err) 37 | assert.Equal(t, res.ID, webhook.ID) 38 | assert.Equal(t, refs.StringValue(res.Endpoint), refs.StringValue(webhook.Endpoint)) 39 | // assert.Equal(t, refs.StringValue(res.EventName), refs.StringValue(webhook.EventName)) 40 | assert.Equal(t, refs.BoolValue(res.Enabled), refs.BoolValue(webhook.Enabled)) 41 | assert.Len(t, res.Headers, len(webhook.Headers)) 42 | } 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /server/smsproviders/twilio.go: -------------------------------------------------------------------------------- 1 | package smsproviders 2 | 3 | import ( 4 | "github.com/authorizerdev/authorizer/server/constants" 5 | "github.com/authorizerdev/authorizer/server/memorystore" 6 | log "github.com/sirupsen/logrus" 7 | twilio "github.com/twilio/twilio-go" 8 | api "github.com/twilio/twilio-go/rest/api/v2010" 9 | ) 10 | 11 | // SendSMS util to send sms 12 | // TODO: Should be restructured to interface when another provider is added 13 | func SendSMS(sendTo, messageBody string) error { 14 | twilioAPISecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwilioAPISecret) 15 | if err != nil || twilioAPISecret == "" { 16 | log.Debug("Failed to get api secret: ", err) 17 | return err 18 | } 19 | twilioAPIKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwilioAPIKey) 20 | if err != nil || twilioAPIKey == "" { 21 | log.Debug("Failed to get api key: ", err) 22 | return err 23 | } 24 | twilioSenderFrom, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwilioSender) 25 | if err != nil || twilioSenderFrom == "" { 26 | log.Debug("Failed to get sender: ", err) 27 | return err 28 | } 29 | // accountSID is not a must to send sms on twilio 30 | twilioAccountSID, _ := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwilioAccountSID) 31 | client := twilio.NewRestClientWithParams(twilio.ClientParams{ 32 | Username: twilioAPIKey, 33 | Password: twilioAPISecret, 34 | AccountSid: twilioAccountSID, 35 | }) 36 | message := &api.CreateMessageParams{} 37 | message.SetBody(messageBody) 38 | message.SetFrom(twilioSenderFrom) 39 | message.SetTo(sendTo) 40 | 41 | _, err = client.Api.CreateMessage(message) 42 | 43 | if err != nil { 44 | log.Debug("Failed to send sms: ", err) 45 | return err 46 | } 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /server/handlers/openid_config.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "github.com/authorizerdev/authorizer/server/constants" 7 | "github.com/authorizerdev/authorizer/server/memorystore" 8 | "github.com/authorizerdev/authorizer/server/parsers" 9 | ) 10 | 11 | // OpenIDConfigurationHandler handler for open-id configurations 12 | func OpenIDConfigurationHandler() gin.HandlerFunc { 13 | return func(c *gin.Context) { 14 | issuer := parsers.GetHost(c) 15 | jwtType, _ := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyJwtType) 16 | 17 | c.JSON(200, gin.H{ 18 | "issuer": issuer, 19 | "authorization_endpoint": issuer + "/authorize", 20 | "token_endpoint": issuer + "/oauth/token", 21 | "userinfo_endpoint": issuer + "/userinfo", 22 | "jwks_uri": issuer + "/.well-known/jwks.json", 23 | "registration_endpoint": issuer + "/app", 24 | "response_types_supported": []string{"code", "token", "id_token"}, 25 | "scopes_supported": []string{"openid", "email", "profile"}, 26 | "response_modes_supported": []string{"query", "fragment", "form_post", "web_message"}, 27 | "subject_types_supported": []string{"public"}, 28 | "id_token_signing_alg_values_supported": []string{jwtType}, 29 | "claims_supported": []string{"aud", "exp", "iss", "iat", "sub", "given_name", "family_name", "middle_name", "nickname", "preferred_username", "picture", "email", "email_verified", "roles", "role", "gender", "birthdate", "phone_number", "phone_number_verified", "nonce", "updated_at", "created_at", "revoked_timestamp", "login_method", "signup_methods", "token_type"}, 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/handlers/userinfo.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | log "github.com/sirupsen/logrus" 9 | 10 | "github.com/authorizerdev/authorizer/server/db" 11 | "github.com/authorizerdev/authorizer/server/token" 12 | ) 13 | 14 | func UserInfoHandler() gin.HandlerFunc { 15 | return func(gc *gin.Context) { 16 | accessToken, err := token.GetAccessToken(gc) 17 | if err != nil { 18 | log.Debug("Error getting access token: ", err) 19 | gc.JSON(http.StatusUnauthorized, gin.H{ 20 | "error": err.Error(), 21 | }) 22 | return 23 | } 24 | claims, err := token.ValidateAccessToken(gc, accessToken) 25 | if err != nil { 26 | log.Debug("Error validating access token: ", err) 27 | gc.JSON(http.StatusUnauthorized, gin.H{ 28 | "error": err.Error(), 29 | }) 30 | return 31 | } 32 | userID := claims["sub"].(string) 33 | user, err := db.Provider.GetUserByID(gc, userID) 34 | if err != nil { 35 | log.Debug("Error getting user: ", err) 36 | gc.JSON(http.StatusUnauthorized, gin.H{ 37 | "error": err.Error(), 38 | }) 39 | return 40 | } 41 | apiUser := user.AsAPIUser() 42 | userBytes, err := json.Marshal(apiUser) 43 | if err != nil { 44 | log.Debug("Error marshalling user: ", err) 45 | gc.JSON(http.StatusUnauthorized, gin.H{ 46 | "error": err.Error(), 47 | }) 48 | return 49 | } 50 | res := map[string]interface{}{} 51 | err = json.Unmarshal(userBytes, &res) 52 | if err != nil { 53 | log.Debug("Error un-marshalling user: ", err) 54 | gc.JSON(http.StatusUnauthorized, gin.H{ 55 | "error": err.Error(), 56 | }) 57 | return 58 | } 59 | // add sub field to user as per openid standards 60 | // https://github.com/authorizerdev/authorizer/issues/327 61 | res["sub"] = userID 62 | gc.JSON(http.StatusOK, res) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /server/db/providers/cassandradb/env.go: -------------------------------------------------------------------------------- 1 | package cassandradb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/authorizerdev/authorizer/server/db/models" 9 | "github.com/gocql/gocql" 10 | "github.com/google/uuid" 11 | ) 12 | 13 | // AddEnv to save environment information in database 14 | func (p *provider) AddEnv(ctx context.Context, env *models.Env) (*models.Env, error) { 15 | if env.ID == "" { 16 | env.ID = uuid.New().String() 17 | } 18 | env.CreatedAt = time.Now().Unix() 19 | env.UpdatedAt = time.Now().Unix() 20 | insertEnvQuery := fmt.Sprintf("INSERT INTO %s (id, env, hash, created_at, updated_at) VALUES ('%s', '%s', '%s', %d, %d)", KeySpace+"."+models.Collections.Env, env.ID, env.EnvData, env.Hash, env.CreatedAt, env.UpdatedAt) 21 | err := p.db.Query(insertEnvQuery).Exec() 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return env, nil 27 | } 28 | 29 | // UpdateEnv to update environment information in database 30 | func (p *provider) UpdateEnv(ctx context.Context, env *models.Env) (*models.Env, error) { 31 | env.UpdatedAt = time.Now().Unix() 32 | updateEnvQuery := fmt.Sprintf("UPDATE %s SET env = '%s', updated_at = %d WHERE id = '%s'", KeySpace+"."+models.Collections.Env, env.EnvData, env.UpdatedAt, env.ID) 33 | err := p.db.Query(updateEnvQuery).Exec() 34 | if err != nil { 35 | return nil, err 36 | } 37 | return env, nil 38 | } 39 | 40 | // GetEnv to get environment information from database 41 | func (p *provider) GetEnv(ctx context.Context) (*models.Env, error) { 42 | var env models.Env 43 | query := fmt.Sprintf("SELECT id, env, hash, created_at, updated_at FROM %s LIMIT 1", KeySpace+"."+models.Collections.Env) 44 | err := p.db.Query(query).Consistency(gocql.One).Scan(&env.ID, &env.EnvData, &env.Hash, &env.CreatedAt, &env.UpdatedAt) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return &env, nil 49 | } 50 | -------------------------------------------------------------------------------- /server/db/providers/provider_template/email_template.go: -------------------------------------------------------------------------------- 1 | package provider_template 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/authorizerdev/authorizer/server/db/models" 8 | "github.com/authorizerdev/authorizer/server/graph/model" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | // AddEmailTemplate to add EmailTemplate 13 | func (p *provider) AddEmailTemplate(ctx context.Context, emailTemplate *models.EmailTemplate) (*model.EmailTemplate, error) { 14 | if emailTemplate.ID == "" { 15 | emailTemplate.ID = uuid.New().String() 16 | } 17 | 18 | emailTemplate.Key = emailTemplate.ID 19 | emailTemplate.CreatedAt = time.Now().Unix() 20 | emailTemplate.UpdatedAt = time.Now().Unix() 21 | return emailTemplate.AsAPIEmailTemplate(), nil 22 | } 23 | 24 | // UpdateEmailTemplate to update EmailTemplate 25 | func (p *provider) UpdateEmailTemplate(ctx context.Context, emailTemplate *models.EmailTemplate) (*model.EmailTemplate, error) { 26 | emailTemplate.UpdatedAt = time.Now().Unix() 27 | return emailTemplate.AsAPIEmailTemplate(), nil 28 | } 29 | 30 | // ListEmailTemplates to list EmailTemplate 31 | func (p *provider) ListEmailTemplate(ctx context.Context, pagination *model.Pagination) (*model.EmailTemplates, error) { 32 | return nil, nil 33 | } 34 | 35 | // GetEmailTemplateByID to get EmailTemplate by id 36 | func (p *provider) GetEmailTemplateByID(ctx context.Context, emailTemplateID string) (*model.EmailTemplate, error) { 37 | return nil, nil 38 | } 39 | 40 | // GetEmailTemplateByEventName to get EmailTemplate by event_name 41 | func (p *provider) GetEmailTemplateByEventName(ctx context.Context, eventName string) (*model.EmailTemplate, error) { 42 | return nil, nil 43 | } 44 | 45 | // DeleteEmailTemplate to delete EmailTemplate 46 | func (p *provider) DeleteEmailTemplate(ctx context.Context, emailTemplate *model.EmailTemplate) error { 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /server/test/deactivate_account_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/db" 9 | "github.com/authorizerdev/authorizer/server/graph/model" 10 | "github.com/authorizerdev/authorizer/server/refs" 11 | "github.com/authorizerdev/authorizer/server/resolvers" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func deactivateAccountTests(t *testing.T, s TestSetup) { 16 | t.Helper() 17 | t.Run(`should deactiavte the user account with access token only`, func(t *testing.T) { 18 | req, ctx := createContext(s) 19 | email := "deactiavte_account." + s.TestInfo.Email 20 | 21 | resolvers.SignupResolver(ctx, model.SignUpInput{ 22 | Email: refs.NewStringRef(email), 23 | Password: s.TestInfo.Password, 24 | ConfirmPassword: s.TestInfo.Password, 25 | }) 26 | _, err := resolvers.DeactivateAccountResolver(ctx) 27 | assert.NotNil(t, err, "unauthorized") 28 | verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeBasicAuthSignup) 29 | assert.NoError(t, err) 30 | assert.NotNil(t, verificationRequest) 31 | verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{ 32 | Token: verificationRequest.Token, 33 | }) 34 | assert.NoError(t, err) 35 | assert.NotNil(t, verifyRes) 36 | s.GinContext.Request.Header.Set("Authorization", "Bearer "+*verifyRes.AccessToken) 37 | ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext) 38 | _, err = resolvers.DeactivateAccountResolver(ctx) 39 | assert.NoError(t, err) 40 | s.GinContext.Request.Header.Set("Authorization", "") 41 | assert.Nil(t, err) 42 | _, err = resolvers.ProfileResolver(ctx) 43 | assert.NotNil(t, err, "unauthorized") 44 | cleanData(email) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /server/test/delete_webhook_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/crypto" 9 | "github.com/authorizerdev/authorizer/server/db" 10 | "github.com/authorizerdev/authorizer/server/graph/model" 11 | "github.com/authorizerdev/authorizer/server/memorystore" 12 | "github.com/authorizerdev/authorizer/server/resolvers" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func deleteWebhookTest(t *testing.T, s TestSetup) { 17 | t.Helper() 18 | t.Run("should delete webhook", func(t *testing.T) { 19 | req, ctx := createContext(s) 20 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 21 | assert.NoError(t, err) 22 | h, err := crypto.EncryptPassword(adminSecret) 23 | assert.NoError(t, err) 24 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 25 | 26 | // get all webhooks 27 | webhooks, err := db.Provider.ListWebhook(ctx, &model.Pagination{ 28 | Limit: 20, 29 | Page: 1, 30 | Offset: 0, 31 | }) 32 | assert.NoError(t, err) 33 | 34 | for _, w := range webhooks.Webhooks { 35 | res, err := resolvers.DeleteWebhookResolver(ctx, model.WebhookRequest{ 36 | ID: w.ID, 37 | }) 38 | 39 | assert.NoError(t, err) 40 | assert.NotNil(t, res) 41 | assert.NotEmpty(t, res.Message) 42 | } 43 | 44 | webhooks, err = db.Provider.ListWebhook(ctx, &model.Pagination{ 45 | Limit: 20, 46 | Page: 1, 47 | Offset: 0, 48 | }) 49 | assert.NoError(t, err) 50 | assert.Len(t, webhooks.Webhooks, 0) 51 | webhookLogs, err := db.Provider.ListWebhookLogs(ctx, &model.Pagination{ 52 | Limit: 100, 53 | Page: 1, 54 | Offset: 0, 55 | }, "") 56 | assert.NoError(t, err) 57 | assert.Len(t, webhookLogs.WebhookLogs, 0) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /server/test/profile_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/db" 9 | "github.com/authorizerdev/authorizer/server/graph/model" 10 | "github.com/authorizerdev/authorizer/server/refs" 11 | "github.com/authorizerdev/authorizer/server/resolvers" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func profileTests(t *testing.T, s TestSetup) { 16 | t.Helper() 17 | t.Run(`should get profile only access_token token`, func(t *testing.T) { 18 | req, ctx := createContext(s) 19 | email := "profile." + s.TestInfo.Email 20 | 21 | resolvers.SignupResolver(ctx, model.SignUpInput{ 22 | Email: refs.NewStringRef(email), 23 | Password: s.TestInfo.Password, 24 | ConfirmPassword: s.TestInfo.Password, 25 | }) 26 | 27 | _, err := resolvers.ProfileResolver(ctx) 28 | assert.NotNil(t, err, "unauthorized") 29 | 30 | verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeBasicAuthSignup) 31 | assert.NoError(t, err) 32 | assert.NotNil(t, verificationRequest) 33 | verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{ 34 | Token: verificationRequest.Token, 35 | }) 36 | assert.NoError(t, err) 37 | assert.NotNil(t, verifyRes.AccessToken) 38 | 39 | s.GinContext.Request.Header.Set("Authorization", "Bearer "+*verifyRes.AccessToken) 40 | ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext) 41 | profileRes, err := resolvers.ProfileResolver(ctx) 42 | assert.Nil(t, err) 43 | assert.NotNil(t, profileRes) 44 | s.GinContext.Request.Header.Set("Authorization", "") 45 | newEmail := profileRes.Email 46 | assert.Equal(t, email, refs.StringValue(newEmail), "emails should be equal") 47 | cleanData(email) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /server/token/admin_token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/authorizerdev/authorizer/server/constants" 7 | "github.com/authorizerdev/authorizer/server/cookie" 8 | "github.com/authorizerdev/authorizer/server/crypto" 9 | "github.com/authorizerdev/authorizer/server/memorystore" 10 | "github.com/gin-gonic/gin" 11 | "golang.org/x/crypto/bcrypt" 12 | ) 13 | 14 | // CreateAdminAuthToken creates the admin token based on secret key 15 | func CreateAdminAuthToken(tokenType string, c *gin.Context) (string, error) { 16 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 17 | if err != nil { 18 | return "", err 19 | } 20 | return crypto.EncryptPassword(adminSecret) 21 | } 22 | 23 | // GetAdminAuthToken helps in getting the admin token from the request cookie 24 | func GetAdminAuthToken(gc *gin.Context) (string, error) { 25 | token, err := cookie.GetAdminCookie(gc) 26 | if err != nil || token == "" { 27 | return "", fmt.Errorf("unauthorized") 28 | } 29 | 30 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 31 | if err != nil { 32 | return "", err 33 | } 34 | err = bcrypt.CompareHashAndPassword([]byte(token), []byte(adminSecret)) 35 | 36 | if err != nil { 37 | return "", fmt.Errorf(`unauthorized`) 38 | } 39 | 40 | return token, nil 41 | } 42 | 43 | // IsSuperAdmin checks if user is super admin 44 | func IsSuperAdmin(gc *gin.Context) bool { 45 | token, err := GetAdminAuthToken(gc) 46 | if err != nil { 47 | secret := gc.Request.Header.Get("x-authorizer-admin-secret") 48 | if secret == "" { 49 | return false 50 | } 51 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 52 | if err != nil { 53 | return false 54 | } 55 | return secret == adminSecret 56 | } 57 | 58 | return token != "" 59 | } 60 | -------------------------------------------------------------------------------- /server/db/models/webhook_log.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/authorizerdev/authorizer/server/graph/model" 7 | "github.com/authorizerdev/authorizer/server/refs" 8 | ) 9 | 10 | // Note: any change here should be reflected in providers/casandra/provider.go as it does not have model support in collection creation 11 | 12 | // WebhookLog model for db 13 | type WebhookLog struct { 14 | Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty" dynamo:"key,omitempty"` // for arangodb 15 | ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id" dynamo:"id,hash"` 16 | HttpStatus int64 `json:"http_status" bson:"http_status" cql:"http_status" dynamo:"http_status"` 17 | Response string `json:"response" bson:"response" cql:"response" dynamo:"response"` 18 | Request string `json:"request" bson:"request" cql:"request" dynamo:"request"` 19 | WebhookID string `gorm:"type:char(36)" json:"webhook_id" bson:"webhook_id" cql:"webhook_id" dynamo:"webhook_id" index:"webhook_id,hash"` 20 | CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at" dynamo:"created_at"` 21 | UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at" dynamo:"updated_at"` 22 | } 23 | 24 | // AsAPIWebhookLog to return webhook log as graphql response object 25 | func (w *WebhookLog) AsAPIWebhookLog() *model.WebhookLog { 26 | id := w.ID 27 | if strings.Contains(id, Collections.WebhookLog+"/") { 28 | id = strings.TrimPrefix(id, Collections.WebhookLog+"/") 29 | } 30 | return &model.WebhookLog{ 31 | ID: id, 32 | HTTPStatus: refs.NewInt64Ref(w.HttpStatus), 33 | Response: refs.NewStringRef(w.Response), 34 | Request: refs.NewStringRef(w.Request), 35 | WebhookID: refs.NewStringRef(w.WebhookID), 36 | CreatedAt: refs.NewInt64Ref(w.CreatedAt), 37 | UpdatedAt: refs.NewInt64Ref(w.UpdatedAt), 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/test/magic_link_login_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/db" 9 | "github.com/authorizerdev/authorizer/server/graph/model" 10 | "github.com/authorizerdev/authorizer/server/memorystore" 11 | "github.com/authorizerdev/authorizer/server/resolvers" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func magicLinkLoginTests(t *testing.T, s TestSetup) { 16 | t.Helper() 17 | t.Run(`should login with magic link`, func(t *testing.T) { 18 | req, ctx := createContext(s) 19 | email := "magic_link_login." + s.TestInfo.Email 20 | memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableSignUp, true) 21 | _, err := resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{ 22 | Email: email, 23 | }) 24 | assert.NotNil(t, err, "signup disabled") 25 | 26 | memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableSignUp, false) 27 | _, err = resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{ 28 | Email: email, 29 | }) 30 | assert.Nil(t, err, "signup should be successful") 31 | 32 | verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeMagicLinkLogin) 33 | assert.NoError(t, err) 34 | assert.NotNil(t, verificationRequest) 35 | verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{ 36 | Token: verificationRequest.Token, 37 | }) 38 | assert.NoError(t, err) 39 | assert.NotNil(t, verifyRes.AccessToken) 40 | s.GinContext.Request.Header.Set("Authorization", "Bearer "+*verifyRes.AccessToken) 41 | ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext) 42 | _, err = resolvers.ProfileResolver(ctx) 43 | assert.Nil(t, err) 44 | s.GinContext.Request.Header.Set("Authorization", "") 45 | cleanData(email) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /server/db/providers/provider_template/verification_requests.go: -------------------------------------------------------------------------------- 1 | package provider_template 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/authorizerdev/authorizer/server/db/models" 8 | "github.com/authorizerdev/authorizer/server/graph/model" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | // AddVerification to save verification request in database 13 | func (p *provider) AddVerificationRequest(ctx context.Context, verificationRequest *models.VerificationRequest) (*models.VerificationRequest, error) { 14 | if verificationRequest.ID == "" { 15 | verificationRequest.ID = uuid.New().String() 16 | } 17 | 18 | verificationRequest.CreatedAt = time.Now().Unix() 19 | verificationRequest.UpdatedAt = time.Now().Unix() 20 | 21 | return verificationRequest, nil 22 | } 23 | 24 | // GetVerificationRequestByToken to get verification request from database using token 25 | func (p *provider) GetVerificationRequestByToken(ctx context.Context, token string) (*models.VerificationRequest, error) { 26 | var verificationRequest *models.VerificationRequest 27 | 28 | return verificationRequest, nil 29 | } 30 | 31 | // GetVerificationRequestByEmail to get verification request by email from database 32 | func (p *provider) GetVerificationRequestByEmail(ctx context.Context, email string, identifier string) (*models.VerificationRequest, error) { 33 | var verificationRequest *models.VerificationRequest 34 | 35 | return verificationRequest, nil 36 | } 37 | 38 | // ListVerificationRequests to get list of verification requests from database 39 | func (p *provider) ListVerificationRequests(ctx context.Context, pagination *model.Pagination) (*model.VerificationRequests, error) { 40 | return nil, nil 41 | } 42 | 43 | // DeleteVerificationRequest to delete verification request from database 44 | func (p *provider) DeleteVerificationRequest(ctx context.Context, verificationRequest *models.VerificationRequest) error { 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /server/test/verification_requests_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/crypto" 9 | "github.com/authorizerdev/authorizer/server/graph/model" 10 | "github.com/authorizerdev/authorizer/server/memorystore" 11 | "github.com/authorizerdev/authorizer/server/refs" 12 | "github.com/authorizerdev/authorizer/server/resolvers" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func verificationRequestsTest(t *testing.T, s TestSetup) { 17 | t.Helper() 18 | 19 | t.Run(`should get verification requests with admin secret only`, func(t *testing.T) { 20 | req, ctx := createContext(s) 21 | email := "verification_requests." + s.TestInfo.Email 22 | res, err := resolvers.SignupResolver(ctx, model.SignUpInput{ 23 | Email: refs.NewStringRef(email), 24 | Password: s.TestInfo.Password, 25 | ConfirmPassword: s.TestInfo.Password, 26 | }) 27 | assert.NoError(t, err) 28 | assert.NotNil(t, res) 29 | limit := int64(10) 30 | page := int64(1) 31 | pagination := &model.PaginatedInput{ 32 | Pagination: &model.PaginationInput{ 33 | Limit: &limit, 34 | Page: &page, 35 | }, 36 | } 37 | 38 | requests, err := resolvers.VerificationRequestsResolver(ctx, pagination) 39 | assert.NotNil(t, err, "unauthorized") 40 | assert.Nil(t, requests) 41 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 42 | assert.Nil(t, err) 43 | 44 | h, err := crypto.EncryptPassword(adminSecret) 45 | assert.Nil(t, err) 46 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 47 | requests, err = resolvers.VerificationRequestsResolver(ctx, pagination) 48 | assert.Nil(t, err) 49 | rLen := len(requests.VerificationRequests) 50 | assert.GreaterOrEqual(t, rLen, 1) 51 | 52 | cleanData(email) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /dashboard/src/components/EnvComponents/OrganizationInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Flex, Stack, Center, Text, useMediaQuery } from '@chakra-ui/react'; 3 | import InputField from '../InputField'; 4 | import { TextInputType } from '../../constants'; 5 | 6 | const OrganizationInfo = ({ variables, setVariables }: any) => { 7 | const [isNotSmallerScreen] = useMediaQuery('(min-width:600px)'); 8 | return ( 9 |
10 | {' '} 11 | 12 | Organization Information 13 | 14 | 15 | 16 | 21 | Organization Name: 22 | 23 |
27 | 33 |
34 |
35 | 36 | 41 | Organization Logo: 42 | 43 |
47 | 53 |
54 |
55 |
56 |
57 | ); 58 | }; 59 | 60 | export default OrganizationInfo; 61 | -------------------------------------------------------------------------------- /server/db/providers/mongodb/env.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/authorizerdev/authorizer/server/db/models" 9 | "github.com/google/uuid" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/mongo/options" 12 | ) 13 | 14 | // AddEnv to save environment information in database 15 | func (p *provider) AddEnv(ctx context.Context, env *models.Env) (*models.Env, error) { 16 | if env.ID == "" { 17 | env.ID = uuid.New().String() 18 | } 19 | env.CreatedAt = time.Now().Unix() 20 | env.UpdatedAt = time.Now().Unix() 21 | env.Key = env.ID 22 | configCollection := p.db.Collection(models.Collections.Env, options.Collection()) 23 | _, err := configCollection.InsertOne(ctx, env) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return env, nil 28 | } 29 | 30 | // UpdateEnv to update environment information in database 31 | func (p *provider) UpdateEnv(ctx context.Context, env *models.Env) (*models.Env, error) { 32 | env.UpdatedAt = time.Now().Unix() 33 | configCollection := p.db.Collection(models.Collections.Env, options.Collection()) 34 | _, err := configCollection.UpdateOne(ctx, bson.M{"_id": bson.M{"$eq": env.ID}}, bson.M{"$set": env}, options.MergeUpdateOptions()) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return env, nil 39 | } 40 | 41 | // GetEnv to get environment information from database 42 | func (p *provider) GetEnv(ctx context.Context) (*models.Env, error) { 43 | var env *models.Env 44 | configCollection := p.db.Collection(models.Collections.Env, options.Collection()) 45 | cursor, err := configCollection.Find(ctx, bson.M{}, options.Find()) 46 | if err != nil { 47 | return nil, err 48 | } 49 | defer cursor.Close(ctx) 50 | for cursor.Next(nil) { 51 | err := cursor.Decode(&env) 52 | if err != nil { 53 | return nil, err 54 | } 55 | } 56 | if env == nil { 57 | return env, fmt.Errorf("config not found") 58 | } 59 | return env, nil 60 | } 61 | -------------------------------------------------------------------------------- /server/test/invite_member_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/crypto" 9 | "github.com/authorizerdev/authorizer/server/graph/model" 10 | "github.com/authorizerdev/authorizer/server/memorystore" 11 | "github.com/authorizerdev/authorizer/server/resolvers" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func inviteUserTest(t *testing.T, s TestSetup) { 16 | t.Helper() 17 | t.Run(`should invite user successfully`, func(t *testing.T) { 18 | req, ctx := createContext(s) 19 | emails := []string{"invite_member1." + s.TestInfo.Email} 20 | 21 | // unauthorized error 22 | res, err := resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{ 23 | Emails: emails, 24 | }) 25 | 26 | assert.Error(t, err) 27 | assert.Nil(t, res) 28 | 29 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 30 | assert.Nil(t, err) 31 | 32 | h, err := crypto.EncryptPassword(adminSecret) 33 | assert.Nil(t, err) 34 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 35 | 36 | // invalid emails test 37 | invalidEmailsTest := []string{ 38 | "test", 39 | "test.com", 40 | } 41 | res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{ 42 | Emails: invalidEmailsTest, 43 | }) 44 | assert.Error(t, err) 45 | assert.Nil(t, res) 46 | // valid test 47 | res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{ 48 | Emails: emails, 49 | }) 50 | assert.Nil(t, err) 51 | assert.NotNil(t, res) 52 | assert.NotNil(t, res.Message) 53 | assert.NotNil(t, res.Users) 54 | // duplicate error test 55 | res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{ 56 | Emails: emails, 57 | }) 58 | assert.Error(t, err) 59 | assert.Nil(t, res) 60 | cleanData(emails[0]) 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /server/handlers/logout.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | log "github.com/sirupsen/logrus" 10 | 11 | "github.com/authorizerdev/authorizer/server/cookie" 12 | "github.com/authorizerdev/authorizer/server/crypto" 13 | "github.com/authorizerdev/authorizer/server/memorystore" 14 | "github.com/authorizerdev/authorizer/server/token" 15 | ) 16 | 17 | // Handler to logout user 18 | func LogoutHandler() gin.HandlerFunc { 19 | return func(gc *gin.Context) { 20 | redirectURL := strings.TrimSpace(gc.Query("redirect_uri")) 21 | // get fingerprint hash 22 | fingerprintHash, err := cookie.GetSession(gc) 23 | if err != nil { 24 | log.Debug("Failed to get session: ", err) 25 | gc.JSON(http.StatusUnauthorized, gin.H{ 26 | "error": err.Error(), 27 | }) 28 | return 29 | } 30 | 31 | decryptedFingerPrint, err := crypto.DecryptAES(fingerprintHash) 32 | if err != nil { 33 | log.Debug("Failed to decrypt fingerprint: ", err) 34 | gc.JSON(http.StatusUnauthorized, gin.H{ 35 | "error": err.Error(), 36 | }) 37 | return 38 | } 39 | 40 | var sessionData token.SessionData 41 | err = json.Unmarshal([]byte(decryptedFingerPrint), &sessionData) 42 | if err != nil { 43 | log.Debug("Failed to decrypt fingerprint: ", err) 44 | gc.JSON(http.StatusUnauthorized, gin.H{ 45 | "error": err.Error(), 46 | }) 47 | return 48 | } 49 | 50 | userID := sessionData.Subject 51 | loginMethod := sessionData.LoginMethod 52 | sessionToken := userID 53 | if loginMethod != "" { 54 | sessionToken = loginMethod + ":" + userID 55 | } 56 | 57 | memorystore.Provider.DeleteUserSession(sessionToken, sessionData.Nonce) 58 | cookie.DeleteSession(gc) 59 | 60 | if redirectURL != "" { 61 | gc.Redirect(http.StatusFound, redirectURL) 62 | } else { 63 | gc.JSON(http.StatusOK, gin.H{ 64 | "message": "Logged out successfully", 65 | }) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /server/db/providers/dynamodb/authenticator.go: -------------------------------------------------------------------------------- 1 | package dynamodb 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | 9 | "github.com/authorizerdev/authorizer/server/db/models" 10 | ) 11 | 12 | func (p *provider) AddAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) { 13 | exists, _ := p.GetAuthenticatorDetailsByUserId(ctx, authenticators.UserID, authenticators.Method) 14 | if exists != nil { 15 | return authenticators, nil 16 | } 17 | 18 | collection := p.db.Table(models.Collections.Authenticators) 19 | if authenticators.ID == "" { 20 | authenticators.ID = uuid.New().String() 21 | } 22 | 23 | authenticators.CreatedAt = time.Now().Unix() 24 | authenticators.UpdatedAt = time.Now().Unix() 25 | err := collection.Put(authenticators).RunWithContext(ctx) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return authenticators, nil 30 | } 31 | 32 | func (p *provider) UpdateAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) { 33 | collection := p.db.Table(models.Collections.Authenticators) 34 | if authenticators.ID != "" { 35 | authenticators.UpdatedAt = time.Now().Unix() 36 | err := UpdateByHashKey(collection, "id", authenticators.ID, authenticators) 37 | if err != nil { 38 | return nil, err 39 | } 40 | } 41 | return authenticators, nil 42 | 43 | } 44 | 45 | func (p *provider) GetAuthenticatorDetailsByUserId(ctx context.Context, userId string, authenticatorType string) (*models.Authenticator, error) { 46 | var authenticators *models.Authenticator 47 | collection := p.db.Table(models.Collections.Authenticators) 48 | iter := collection.Scan().Filter("'user_id' = ?", userId).Filter("'method' = ?", authenticatorType).Iter() 49 | for iter.NextWithContext(ctx, &authenticators) { 50 | return authenticators, nil 51 | } 52 | err := iter.Err() 53 | if err != nil { 54 | return nil, err 55 | } 56 | return authenticators, nil 57 | } 58 | -------------------------------------------------------------------------------- /server/db/providers/arangodb/env.go: -------------------------------------------------------------------------------- 1 | package arangodb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | arangoDriver "github.com/arangodb/go-driver" 9 | "github.com/google/uuid" 10 | 11 | "github.com/authorizerdev/authorizer/server/db/models" 12 | ) 13 | 14 | // AddEnv to save environment information in database 15 | func (p *provider) AddEnv(ctx context.Context, env *models.Env) (*models.Env, error) { 16 | if env.ID == "" { 17 | env.ID = uuid.New().String() 18 | env.Key = env.ID 19 | } 20 | 21 | env.CreatedAt = time.Now().Unix() 22 | env.UpdatedAt = time.Now().Unix() 23 | configCollection, _ := p.db.Collection(ctx, models.Collections.Env) 24 | meta, err := configCollection.CreateDocument(arangoDriver.WithOverwrite(ctx), env) 25 | if err != nil { 26 | return nil, err 27 | } 28 | env.Key = meta.Key 29 | env.ID = meta.ID.String() 30 | return env, nil 31 | } 32 | 33 | // UpdateEnv to update environment information in database 34 | func (p *provider) UpdateEnv(ctx context.Context, env *models.Env) (*models.Env, error) { 35 | env.UpdatedAt = time.Now().Unix() 36 | collection, _ := p.db.Collection(ctx, models.Collections.Env) 37 | meta, err := collection.UpdateDocument(ctx, env.Key, env) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | env.Key = meta.Key 43 | env.ID = meta.ID.String() 44 | return env, nil 45 | } 46 | 47 | // GetEnv to get environment information from database 48 | func (p *provider) GetEnv(ctx context.Context) (*models.Env, error) { 49 | var env *models.Env 50 | query := fmt.Sprintf("FOR d in %s RETURN d", models.Collections.Env) 51 | cursor, err := p.db.Query(ctx, query, nil) 52 | if err != nil { 53 | return nil, err 54 | } 55 | defer cursor.Close() 56 | for { 57 | if !cursor.HasMore() { 58 | if env == nil { 59 | return env, fmt.Errorf("config not found") 60 | } 61 | break 62 | } 63 | _, err := cursor.ReadDocument(ctx, &env) 64 | if err != nil { 65 | return nil, err 66 | } 67 | } 68 | 69 | return env, nil 70 | } 71 | -------------------------------------------------------------------------------- /server/db/providers/provider_template/webhook.go: -------------------------------------------------------------------------------- 1 | package provider_template 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/authorizerdev/authorizer/server/db/models" 10 | "github.com/authorizerdev/authorizer/server/graph/model" 11 | "github.com/google/uuid" 12 | ) 13 | 14 | // AddWebhook to add webhook 15 | func (p *provider) AddWebhook(ctx context.Context, webhook *models.Webhook) (*model.Webhook, error) { 16 | if webhook.ID == "" { 17 | webhook.ID = uuid.New().String() 18 | } 19 | webhook.Key = webhook.ID 20 | webhook.CreatedAt = time.Now().Unix() 21 | webhook.UpdatedAt = time.Now().Unix() 22 | // Add timestamp to make event name unique for legacy version 23 | webhook.EventName = fmt.Sprintf("%s-%d", webhook.EventName, time.Now().Unix()) 24 | return webhook.AsAPIWebhook(), nil 25 | } 26 | 27 | // UpdateWebhook to update webhook 28 | func (p *provider) UpdateWebhook(ctx context.Context, webhook *models.Webhook) (*model.Webhook, error) { 29 | webhook.UpdatedAt = time.Now().Unix() 30 | // Event is changed 31 | if !strings.Contains(webhook.EventName, "-") { 32 | webhook.EventName = fmt.Sprintf("%s-%d", webhook.EventName, time.Now().Unix()) 33 | } 34 | return webhook.AsAPIWebhook(), nil 35 | } 36 | 37 | // ListWebhooks to list webhook 38 | func (p *provider) ListWebhook(ctx context.Context, pagination *model.Pagination) (*model.Webhooks, error) { 39 | return nil, nil 40 | } 41 | 42 | // GetWebhookByID to get webhook by id 43 | func (p *provider) GetWebhookByID(ctx context.Context, webhookID string) (*model.Webhook, error) { 44 | return nil, nil 45 | } 46 | 47 | // GetWebhookByEventName to get webhook by event_name 48 | func (p *provider) GetWebhookByEventName(ctx context.Context, eventName string) ([]*model.Webhook, error) { 49 | return nil, nil 50 | } 51 | 52 | // DeleteWebhook to delete webhook 53 | func (p *provider) DeleteWebhook(ctx context.Context, webhook *model.Webhook) error { 54 | // Also delete webhook logs for given webhook id 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /server/test/webhook_logs_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/authorizerdev/authorizer/server/constants" 9 | "github.com/authorizerdev/authorizer/server/crypto" 10 | "github.com/authorizerdev/authorizer/server/graph/model" 11 | "github.com/authorizerdev/authorizer/server/memorystore" 12 | "github.com/authorizerdev/authorizer/server/refs" 13 | "github.com/authorizerdev/authorizer/server/resolvers" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func webhookLogsTest(t *testing.T, s TestSetup) { 18 | time.Sleep(30 * time.Second) // add sleep for webhooklogs to get generated as they are async 19 | t.Helper() 20 | t.Run("should get webhook logs", func(t *testing.T) { 21 | req, ctx := createContext(s) 22 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 23 | assert.NoError(t, err) 24 | h, err := crypto.EncryptPassword(adminSecret) 25 | assert.NoError(t, err) 26 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 27 | 28 | webhookLogs, err := resolvers.WebhookLogsResolver(ctx, nil) 29 | assert.NoError(t, err) 30 | assert.Greater(t, len(webhookLogs.WebhookLogs), 1) 31 | 32 | webhooks, err := resolvers.WebhooksResolver(ctx, &model.PaginatedInput{ 33 | Pagination: &model.PaginationInput{ 34 | Limit: refs.NewInt64Ref(20), 35 | }, 36 | }) 37 | assert.NoError(t, err) 38 | assert.NotEmpty(t, webhooks) 39 | for _, w := range webhooks.Webhooks { 40 | t.Run(fmt.Sprintf("should get webhook for webhook_id:%s", w.ID), func(t *testing.T) { 41 | webhookLogs, err := resolvers.WebhookLogsResolver(ctx, &model.ListWebhookLogRequest{ 42 | WebhookID: &w.ID, 43 | }) 44 | assert.NoError(t, err) 45 | assert.GreaterOrEqual(t, len(webhookLogs.WebhookLogs), 1, refs.StringValue(w.EventName)) 46 | for _, wl := range webhookLogs.WebhookLogs { 47 | assert.Equal(t, refs.StringValue(wl.WebhookID), w.ID) 48 | } 49 | }) 50 | } 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /server/test/update_profile_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/authorizerdev/authorizer/server/constants" 8 | "github.com/authorizerdev/authorizer/server/db" 9 | "github.com/authorizerdev/authorizer/server/graph/model" 10 | "github.com/authorizerdev/authorizer/server/refs" 11 | "github.com/authorizerdev/authorizer/server/resolvers" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func updateProfileTests(t *testing.T, s TestSetup) { 16 | t.Helper() 17 | t.Run(`should update the profile with access token only`, func(t *testing.T) { 18 | req, ctx := createContext(s) 19 | email := "update_profile." + s.TestInfo.Email 20 | 21 | resolvers.SignupResolver(ctx, model.SignUpInput{ 22 | Email: refs.NewStringRef(email), 23 | Password: s.TestInfo.Password, 24 | ConfirmPassword: s.TestInfo.Password, 25 | }) 26 | 27 | fName := "samani" 28 | _, err := resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{ 29 | FamilyName: &fName, 30 | }) 31 | assert.NotNil(t, err, "unauthorized") 32 | 33 | verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeBasicAuthSignup) 34 | assert.NoError(t, err) 35 | assert.NotNil(t, verificationRequest) 36 | verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{ 37 | Token: verificationRequest.Token, 38 | }) 39 | assert.NoError(t, err) 40 | assert.NotNil(t, verifyRes) 41 | s.GinContext.Request.Header.Set("Authorization", "Bearer "+*verifyRes.AccessToken) 42 | ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext) 43 | 44 | newEmail := "new_" + email 45 | _, err = resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{ 46 | Email: &newEmail, 47 | }) 48 | s.GinContext.Request.Header.Set("Authorization", "") 49 | assert.Nil(t, err) 50 | _, err = resolvers.ProfileResolver(ctx) 51 | assert.NotNil(t, err, "unauthorized") 52 | 53 | cleanData(newEmail) 54 | cleanData(email) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import { AuthorizerProvider } from '@authorizerdev/authorizer-react'; 4 | import Root from './Root'; 5 | import { createRandomString } from './utils/common'; 6 | 7 | declare global { 8 | interface Window { 9 | __authorizer__: any; 10 | } 11 | } 12 | 13 | export default function App() { 14 | const searchParams = new URLSearchParams(window.location.search); 15 | const state = searchParams.get('state') || createRandomString(); 16 | const scope = searchParams.get('scope') 17 | ? searchParams.get('scope')?.toString().split(' ') 18 | : `openid profile email`; 19 | 20 | const urlProps: Record = { 21 | state, 22 | scope, 23 | }; 24 | 25 | const redirectURL = 26 | searchParams.get('redirect_uri') || searchParams.get('redirectURL'); 27 | if (redirectURL) { 28 | urlProps.redirectURL = redirectURL; 29 | } else { 30 | urlProps.redirectURL = window.location.href; 31 | } 32 | const globalState: Record = { 33 | ...window['__authorizer__'], 34 | ...urlProps, 35 | }; 36 | return ( 37 |
44 |
53 | logo 58 |

{globalState.organizationName}

59 |
60 |
61 | 62 | 68 | 69 | 70 | 71 |
72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /server/test/add_webhook_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/authorizerdev/authorizer/server/constants" 9 | "github.com/authorizerdev/authorizer/server/crypto" 10 | "github.com/authorizerdev/authorizer/server/graph/model" 11 | "github.com/authorizerdev/authorizer/server/memorystore" 12 | "github.com/authorizerdev/authorizer/server/refs" 13 | "github.com/authorizerdev/authorizer/server/resolvers" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func addWebhookTest(t *testing.T, s TestSetup) { 18 | t.Helper() 19 | t.Run("should add webhook", func(t *testing.T) { 20 | req, ctx := createContext(s) 21 | adminSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) 22 | assert.NoError(t, err) 23 | h, err := crypto.EncryptPassword(adminSecret) 24 | assert.NoError(t, err) 25 | req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.AdminCookieName, h)) 26 | for _, eventType := range s.TestInfo.TestWebhookEventTypes { 27 | webhook, err := resolvers.AddWebhookResolver(ctx, model.AddWebhookRequest{ 28 | EventName: eventType, 29 | Endpoint: s.TestInfo.WebhookEndpoint, 30 | Enabled: true, 31 | Headers: map[string]interface{}{ 32 | "x-test": "foo", 33 | }, 34 | }) 35 | assert.NoError(t, err) 36 | assert.NotNil(t, webhook) 37 | assert.NotEmpty(t, webhook.Message) 38 | } 39 | time.Sleep(2 * time.Second) 40 | // Allow setting multiple webhooks for same event 41 | for _, eventType := range s.TestInfo.TestWebhookEventTypes { 42 | webhook, err := resolvers.AddWebhookResolver(ctx, model.AddWebhookRequest{ 43 | EventName: eventType, 44 | Endpoint: s.TestInfo.WebhookEndpoint, 45 | Enabled: true, 46 | EventDescription: refs.NewStringRef(eventType + "-2"), 47 | Headers: map[string]interface{}{ 48 | "x-test": "foo", 49 | }, 50 | }) 51 | assert.NoError(t, err) 52 | assert.NotNil(t, webhook) 53 | assert.NotEmpty(t, webhook.Message) 54 | } 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /server/cookie/admin_cookie.go: -------------------------------------------------------------------------------- 1 | package cookie 2 | 3 | import ( 4 | "net/url" 5 | 6 | log "github.com/sirupsen/logrus" 7 | 8 | "github.com/authorizerdev/authorizer/server/constants" 9 | "github.com/authorizerdev/authorizer/server/memorystore" 10 | "github.com/authorizerdev/authorizer/server/parsers" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | // SetAdminCookie sets the admin cookie in the response 15 | func SetAdminCookie(gc *gin.Context, token string) { 16 | adminCookieSecure, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyAdminCookieSecure) 17 | if err != nil { 18 | log.Debug("Error while getting admin cookie secure from env variable: %v", err) 19 | adminCookieSecure = true 20 | } 21 | 22 | secure := adminCookieSecure 23 | httpOnly := adminCookieSecure 24 | hostname := parsers.GetHost(gc) 25 | host, _ := parsers.GetHostParts(hostname) 26 | gc.SetCookie(constants.AdminCookieName, token, 3600, "/", host, secure, httpOnly) 27 | } 28 | 29 | // GetAdminCookie gets the admin cookie from the request 30 | func GetAdminCookie(gc *gin.Context) (string, error) { 31 | cookie, err := gc.Request.Cookie(constants.AdminCookieName) 32 | if err != nil { 33 | return "", err 34 | } 35 | 36 | // cookie escapes special characters like $ 37 | // hence we need to unescape before comparing 38 | decodedValue, err := url.QueryUnescape(cookie.Value) 39 | if err != nil { 40 | return "", err 41 | } 42 | return decodedValue, nil 43 | } 44 | 45 | // DeleteAdminCookie sets the response cookie to empty 46 | func DeleteAdminCookie(gc *gin.Context) { 47 | adminCookieSecure, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyAdminCookieSecure) 48 | if err != nil { 49 | log.Debug("Error while getting admin cookie secure from env variable: %v", err) 50 | adminCookieSecure = true 51 | } 52 | 53 | secure := adminCookieSecure 54 | httpOnly := adminCookieSecure 55 | hostname := parsers.GetHost(gc) 56 | host, _ := parsers.GetHostParts(hostname) 57 | gc.SetCookie(constants.AdminCookieName, "", -1, "/", host, secure, httpOnly) 58 | } 59 | -------------------------------------------------------------------------------- /server/resolvers/add_email_template.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/authorizerdev/authorizer/server/db" 9 | "github.com/authorizerdev/authorizer/server/db/models" 10 | "github.com/authorizerdev/authorizer/server/graph/model" 11 | "github.com/authorizerdev/authorizer/server/refs" 12 | "github.com/authorizerdev/authorizer/server/token" 13 | "github.com/authorizerdev/authorizer/server/utils" 14 | "github.com/authorizerdev/authorizer/server/validators" 15 | log "github.com/sirupsen/logrus" 16 | ) 17 | 18 | // AddEmailTemplateResolver resolver for add email template mutation 19 | func AddEmailTemplateResolver(ctx context.Context, params model.AddEmailTemplateRequest) (*model.Response, error) { 20 | gc, err := utils.GinContextFromContext(ctx) 21 | if err != nil { 22 | log.Debug("Failed to get GinContext: ", err) 23 | return nil, err 24 | } 25 | 26 | if !token.IsSuperAdmin(gc) { 27 | log.Debug("Not logged in as super admin") 28 | return nil, fmt.Errorf("unauthorized") 29 | } 30 | 31 | if !validators.IsValidEmailTemplateEventName(params.EventName) { 32 | log.Debug("Invalid Event Name: ", params.EventName) 33 | return nil, fmt.Errorf("invalid event name %s", params.EventName) 34 | } 35 | 36 | if strings.TrimSpace(params.Subject) == "" { 37 | return nil, fmt.Errorf("empty subject not allowed") 38 | } 39 | 40 | if strings.TrimSpace(params.Template) == "" { 41 | return nil, fmt.Errorf("empty template not allowed") 42 | } 43 | 44 | var design string 45 | 46 | if params.Design == nil || strings.TrimSpace(refs.StringValue(params.Design)) == "" { 47 | design = "" 48 | } 49 | 50 | _, err = db.Provider.AddEmailTemplate(ctx, &models.EmailTemplate{ 51 | EventName: params.EventName, 52 | Template: params.Template, 53 | Subject: params.Subject, 54 | Design: design, 55 | }) 56 | if err != nil { 57 | log.Debug("Failed to add email template: ", err) 58 | return nil, err 59 | } 60 | 61 | return &model.Response{ 62 | Message: `Email template added successfully`, 63 | }, nil 64 | } 65 | -------------------------------------------------------------------------------- /server/db/providers/couchbase/env.go: -------------------------------------------------------------------------------- 1 | package couchbase 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/authorizerdev/authorizer/server/db/models" 9 | "github.com/couchbase/gocb/v2" 10 | "github.com/google/uuid" 11 | ) 12 | 13 | // AddEnv to save environment information in database 14 | func (p *provider) AddEnv(ctx context.Context, env *models.Env) (*models.Env, error) { 15 | if env.ID == "" { 16 | env.ID = uuid.New().String() 17 | } 18 | env.CreatedAt = time.Now().Unix() 19 | env.UpdatedAt = time.Now().Unix() 20 | env.Key = env.ID 21 | env.EncryptionKey = env.Hash 22 | insertOpt := gocb.InsertOptions{ 23 | Context: ctx, 24 | } 25 | _, err := p.db.Collection(models.Collections.Env).Insert(env.ID, env, &insertOpt) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return env, nil 30 | } 31 | 32 | // UpdateEnv to update environment information in database 33 | func (p *provider) UpdateEnv(ctx context.Context, env *models.Env) (*models.Env, error) { 34 | env.UpdatedAt = time.Now().Unix() 35 | env.EncryptionKey = env.Hash 36 | 37 | updateEnvQuery := fmt.Sprintf("UPDATE %s.%s SET env = $1, updated_at = $2 WHERE _id = $3", p.scopeName, models.Collections.Env) 38 | _, err := p.db.Query(updateEnvQuery, &gocb.QueryOptions{ 39 | Context: ctx, 40 | PositionalParameters: []interface{}{env.EnvData, env.UpdatedAt, env.UpdatedAt, env.ID}, 41 | }) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return env, nil 46 | } 47 | 48 | // GetEnv to get environment information from database 49 | func (p *provider) GetEnv(ctx context.Context) (*models.Env, error) { 50 | var env *models.Env 51 | 52 | query := fmt.Sprintf("SELECT _id, env, encryption_key, created_at, updated_at FROM %s.%s LIMIT 1", p.scopeName, models.Collections.Env) 53 | q, err := p.db.Query(query, &gocb.QueryOptions{ 54 | Context: ctx, 55 | ScanConsistency: gocb.QueryScanConsistencyRequestPlus, 56 | }) 57 | if err != nil { 58 | return nil, err 59 | } 60 | err = q.One(&env) 61 | if err != nil { 62 | return nil, err 63 | } 64 | env.Hash = env.EncryptionKey 65 | return env, nil 66 | } 67 | -------------------------------------------------------------------------------- /server/memorystore/providers/providers.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | // Provider defines current memory store provider 4 | type Provider interface { 5 | // SetUserSession sets the user session for given user identifier in form recipe:user_id 6 | SetUserSession(userId, key, token string, expiration int64) error 7 | // GetUserSession returns the session token for given token 8 | GetUserSession(userId, key string) (string, error) 9 | // DeleteUserSession deletes the user session 10 | DeleteUserSession(userId, key string) error 11 | // DeleteAllSessions deletes all the sessions from the session store 12 | DeleteAllUserSessions(userId string) error 13 | // DeleteSessionForNamespace deletes the session for a given namespace 14 | DeleteSessionForNamespace(namespace string) error 15 | // SetMfaSession sets the mfa session with key and value of userId 16 | SetMfaSession(userId, key string, expiration int64) error 17 | // GetMfaSession returns value of given mfa session 18 | GetMfaSession(userId, key string) (string, error) 19 | // DeleteMfaSession deletes given mfa session from in-memory store. 20 | DeleteMfaSession(userId, key string) error 21 | 22 | // SetState sets the login state (key, value form) in the session store 23 | SetState(key, state string) error 24 | // GetState returns the state from the session store 25 | GetState(key string) (string, error) 26 | // RemoveState removes the social login state from the session store 27 | RemoveState(key string) error 28 | 29 | // methods for env store 30 | 31 | // UpdateEnvStore to update the whole env store object 32 | UpdateEnvStore(store map[string]interface{}) error 33 | // GetEnvStore() returns the env store object 34 | GetEnvStore() (map[string]interface{}, error) 35 | // UpdateEnvVariable to update the particular env variable 36 | UpdateEnvVariable(key string, value interface{}) error 37 | // GetStringStoreEnvVariable to get the string env variable from env store 38 | GetStringStoreEnvVariable(key string) (string, error) 39 | // GetBoolStoreEnvVariable to get the bool env variable from env store 40 | GetBoolStoreEnvVariable(key string) (bool, error) 41 | } 42 | -------------------------------------------------------------------------------- /server/db/providers/mongodb/authenticator.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | "go.mongodb.org/mongo-driver/bson" 9 | "go.mongodb.org/mongo-driver/mongo/options" 10 | 11 | "github.com/authorizerdev/authorizer/server/db/models" 12 | ) 13 | 14 | func (p *provider) AddAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) { 15 | exists, _ := p.GetAuthenticatorDetailsByUserId(ctx, authenticators.UserID, authenticators.Method) 16 | if exists != nil { 17 | return authenticators, nil 18 | } 19 | 20 | if authenticators.ID == "" { 21 | authenticators.ID = uuid.New().String() 22 | } 23 | authenticators.CreatedAt = time.Now().Unix() 24 | authenticators.UpdatedAt = time.Now().Unix() 25 | authenticators.Key = authenticators.ID 26 | authenticatorsCollection := p.db.Collection(models.Collections.Authenticators, options.Collection()) 27 | _, err := authenticatorsCollection.InsertOne(ctx, authenticators) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return authenticators, nil 32 | } 33 | 34 | func (p *provider) UpdateAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) { 35 | authenticators.UpdatedAt = time.Now().Unix() 36 | authenticatorsCollection := p.db.Collection(models.Collections.Authenticators, options.Collection()) 37 | _, err := authenticatorsCollection.UpdateOne(ctx, bson.M{"_id": bson.M{"$eq": authenticators.ID}}, bson.M{"$set": authenticators}) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return authenticators, nil 42 | } 43 | 44 | func (p *provider) GetAuthenticatorDetailsByUserId(ctx context.Context, userId string, authenticatorType string) (*models.Authenticator, error) { 45 | var authenticators *models.Authenticator 46 | authenticatorsCollection := p.db.Collection(models.Collections.Authenticators, options.Collection()) 47 | err := authenticatorsCollection.FindOne(ctx, bson.M{"user_id": userId, "method": authenticatorType}).Decode(&authenticators) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return authenticators, nil 52 | } 53 | --------------------------------------------------------------------------------