Compare commits

...

26 Commits

Author SHA1 Message Date
Lakhan Samani
bedc3d0b50 fix: arangodb get one queries 2021-12-20 17:33:11 +05:30
Lakhan Samani
1398762e1d fix: remove completed todo 2021-12-19 05:46:14 +05:30
Lakhan Samani
e0a77da773 test: add email validtor test 2021-12-19 05:45:06 +05:30
Lakhan Samani
c3f4cd3bf9 feat: add support for sqlserver (#81)
* feat: add support for sqlserver

* fix: update gorm dependencies

* fix: update constraint
2021-12-17 21:50:57 +05:30
Lakhan Samani
f110255310 feat/add arangodb support (#80)
*feat: add arangodb init

* fix: dao for sql + arangodb

* fix: update error logs

* fix: use db name dynamically
2021-12-17 21:25:07 +05:30
Lakhan Samani
155d2e65c2 fix: use char(36) with golang uuid instead of sql uuid type (#78)
resolves #77
2021-12-14 22:57:45 +05:30
Lakhan Samani
4d341e9876 feat: add instruction for railway.app 2021-12-11 23:07:19 +05:30
Lakhan Samani
1761f41691 fix: read cookieName-client if cookie with cookieName is not present 2021-12-11 06:45:15 +05:30
Lakhan Samani
00565c8717 Fix/cookie host (#76)
* fix: cookie host

* feat: add test for url utils

* fix: url test

* fix: multi domain cookie if allowed
2021-12-11 06:41:35 +05:30
Lakhan Samani
74a551ae09 Update README.md 2021-12-09 09:17:32 +05:30
Lakhan Samani
cb5b02d777 fix: update discord link
fix: redirect link for verification handler (#74)

Resolves #70
2021-12-08 18:10:11 +05:30
Lakhan Samani
6ca37a0d50 feat: add oidc for google login 2021-12-03 22:55:27 +05:30
Lakhan Samani
a9cf301344 feat: update app dependencies 2021-11-15 04:12:28 +05:30
Lakhan Samani
0305a719db feat: add support for magic link login (#65)
* feat: add support for magic link login

* update readme
2021-11-12 05:22:03 +05:30
Lakhan Samani
4269e2242c fix: sample script 2021-10-31 11:06:07 +05:30
Lakhan Samani
71e4e35de6 add sample for custom access token script 2021-10-31 11:05:06 +05:30
Lakhan Samani
3aeb3b8d67 fix: remove log 2021-10-30 16:46:37 +05:30
Lakhan Samani
e1951bfbe0 fix: restore release 2021-10-29 21:47:28 +05:30
Lakhan Samani
1299ec5f9c fix: enable win release 2021-10-29 21:43:38 +05:30
Lakhan Samani
bc53974d2a fix: use otto instead of v8go 2021-10-29 21:41:47 +05:30
Lakhan Samani
3ed5426467 chore: fix env for release 2021-10-28 07:46:56 +05:30
Lakhan Samani
29251c8c20 fix: use inbuilt actions 2021-10-28 07:41:49 +05:30
Lakhan Samani
08b1f97ccb chore: fix docker build 2021-10-28 07:17:44 +05:30
Lakhan Samani
abc2991e1c Merge branch 'main' of https://github.com/authorizerdev/authorizer 2021-10-27 23:16:15 +05:30
Lakhan Samani
b69d0b8e23 Feat/multiple session (#64)
* fix: disable windows build

* feat: add ability to handle multiple sessions
2021-10-27 23:15:38 +05:30
Lakhan Samani
86d781b210 fix: disable windows build 2021-10-27 12:10:44 +05:30
58 changed files with 1659 additions and 1818 deletions

View File

@@ -9,3 +9,4 @@ ROLES=user
DEFAULT_ROLES=user
PROTECTED_ROLES=admin
JWT_ROLE_CLAIM=role
CUSTOM_ACCESS_TOKEN_SCRIPT=function(user,tokenPayload){var data = tokenPayload;data.extra = {'x-extra-id': user.id};return data;}

View File

@@ -10,7 +10,7 @@ We're so excited you're interested in helping with Authorizer! We are happy to h
## Where to ask questions?
1. Check our [Github Issues](https://github.com/authorizerdev/authorizer/issues) to see if someone has already answered your question.
2. Join our community on [Discord](https://discord.gg/WDvCxwkX) and feel free to ask us your questions
2. Join our community on [Discord](https://discord.gg/Zv2D5h6kkK) and feel free to ask us your questions
As you gain experience with Authorizer, please help answer other people's questions! :pray:
@@ -19,7 +19,7 @@ As you gain experience with Authorizer, please help answer other people's questi
You can get started by taking a look at our [Github issues](https://github.com/authorizerdev/authorizer/issues)
If you find one that looks interesting and no one else is already working on it, comment on that issue and start contributing 🙂.
Please ask as many questions as you need, either directly in the issue or on [Discord](https://discord.gg/WDvCxwkX). We're happy to help!:raised_hands:
Please ask as many questions as you need, either directly in the issue or on [Discord](https://discord.gg/Zv2D5h6kkK). We're happy to help!:raised_hands:
### Contributions that are ALWAYS welcome

View File

@@ -1,4 +1,4 @@
FROM golang:1.16-alpine as builder
FROM golang:1.17-alpine as builder
WORKDIR /app
COPY server server
COPY Makefile .
@@ -7,7 +7,7 @@ ARG VERSION="latest"
ENV VERSION="$VERSION"
RUN echo "$VERSION"
RUN apk add build-base &&\
RUN apk add build-base nodejs &&\
make clean && make && \
chmod 777 build/server

View File

@@ -5,3 +5,5 @@ cmd:
cd server && go build -ldflags "-w -X main.Version=$(VERSION)" -o '../build/server'
clean:
rm -rf build
test:
cd server && go clean --testcache && go test ./...

View File

@@ -15,7 +15,7 @@
- [Getting Started](#getting-started)
- [Contributing](https://github.com/authorizerdev/authorizer/blob/main/.github/CONTRIBUTING.md)
- [Docs](http://docs.authorizer.dev/)
- [Join Community](https://discord.gg/2fXUQN3E)
- [Join Community](https://discord.gg/Zv2D5h6kkK)
# Introduction
@@ -30,14 +30,10 @@
- ✅ Forgot password flow using email
- ✅ Social logins (Google, Github, Facebook, more coming soon)
- ✅ Role-based access management
## Project Status
⚠️ **Authorizer is still an early beta! missing features and bugs are to be expected!** If you can stomach it, then bring authentication and authorization to your site today!
- ✅ Password-less login with email and magic link
## Roadmap
- Password-less login with email and magic link
- Support more JWT encryption algorithms (Currently supporting HS256)
- 2 Factor authentication
- Back office (Admin dashboard to manage user)
@@ -70,6 +66,7 @@ This guide helps you practice using Authorizer to evaluate it before you use it
- [Install using source code](#install-using-source-code)
- [Install using binaries](#install-using-binaries)
- [Install instance on heroku](#install-instance-on-Heroku)
- [Install instance on railway.app](#install-instance-on-railway)
## Install using source code
@@ -95,13 +92,12 @@ binaries are baked with required deployment files and bundled. You can download
- Mac OSX
- Linux
- Windows
### Step 1: Download and unzip bundle
- Download the Bundle for the specific OS from the [release page](https://github.com/authorizerdev/authorizer/releases)
> Note: For windows, it includes `.zip` file. For Linux & MacOS, it includes `.tar.gz` file.
> Note: For windows, we recommend running using docker image to run authorizer.
- Unzip using following command
@@ -111,12 +107,6 @@ binaries are baked with required deployment files and bundled. You can download
tar -zxf AUTHORIZER_VERSION -c authorizer
```
- Windows
```sh
unzip AUTHORIZER_VERSION
```
- Change directory to `authorizer`
```sh
@@ -137,12 +127,6 @@ Required environment variables are pre-configured in `.env` file. But based on t
./build/server
```
- For windows
```sh
./build/server.exe
```
> Note: For mac users, you might have to give binary the permission to execute. Here is the command you can use to grant permission `xattr -d com.apple.quarantine build/server`
## Install instance on Heroku
@@ -151,12 +135,19 @@ Deploy Authorizer using [heroku](https://github.com/authorizerdev/authorizer-her
<br/><br/>
[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku)
# Install instance on railway
Deploy production ready Authorizer instance using [railway.app](https://github.com/authorizerdev/authorizer-railway) with postgres and redis for free and build with it in 30seconds
<br/>
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template?template=https%3A%2F%2Fgithub.com%2Fauthorizerdev%2Fauthorizer-railway&plugins=postgresql%2Credis&envs=ENV%2CDATABASE_TYPE%2CADMIN_SECRET%2CCOOKIE_NAME%2CJWT_ROLE_CLAIM%2CJWT_TYPE%2CJWT_SECRET%2CFACEBOOK_CLIENT_ID%2CFACEBOOK_CLIENT_SECRET%2CGOOGLE_CLIENT_ID%2CGOOGLE_CLIENT_SECRET%2CGITHUB_CLIENT_ID%2CGITHUB_CLIENT_SECRET%2CALLOWED_ORIGINS%2CROLES%2CPROTECTED_ROLES%2CDEFAULT_ROLES&optionalEnvs=FACEBOOK_CLIENT_ID%2CFACEBOOK_CLIENT_SECRET%2CGOOGLE_CLIENT_ID%2CGOOGLE_CLIENT_SECRET%2CGITHUB_CLIENT_ID%2CGITHUB_CLIENT_SECRET%2CALLOWED_ORIGINS%2CROLES%2CPROTECTED_ROLES%2CDEFAULT_ROLES&ENVDesc=Deployment+environment&DATABASE_TYPEDesc=With+railway+we+are+deploying+postgres+db&ADMIN_SECRETDesc=Secret+to+access+the+admin+apis&COOKIE_NAMEDesc=Name+of+http+only+cookie+that+will+be+used+as+session&FACEBOOK_CLIENT_IDDesc=Facebook+client+ID+for+facebook+login&FACEBOOK_CLIENT_SECRETDesc=Facebook+client+secret+for+facebook+login&GOOGLE_CLIENT_IDDesc=Google+client+ID+for+google+login&GOOGLE_CLIENT_SECRETDesc=Google+client+secret+for+google+login&GITHUB_CLIENT_IDDesc=Github+client+ID+for+github+login&GITHUB_CLIENT_SECRETDesc=Github+client+secret+for+github+login&ALLOWED_ORIGINSDesc=Whitelist+the+URL+for+which+this+instance+of+authorizer+is+allowed&ROLESDesc=Comma+separated+list+of+roles+that+platform+supports.+Default+role+is+user&PROTECTED_ROLESDesc=Comma+separated+list+of+protected+roles+for+which+sign-up+is+disabled&DEFAULT_ROLESDesc=Default+role+that+should+be+assigned+to+user.+It+should+be+one+from+the+list+of+%60ROLES%60+env.+Default+role+is+user&JWT_ROLE_CLAIMDesc=JWT+key+to+be+used+to+validate+the+role+field.&JWT_TYPEDesc=JWT+encryption+type&JWT_SECRETDesc=Random+string+that+will+be+used+for+encrypting+the+JWT+token&ENVDefault=PRODUCTION&DATABASE_TYPEDefault=postgres&COOKIE_NAMEDefault=authorizer&JWT_TYPEDefault=HS256&JWT_ROLE_CLAIMDefault=role)
### Things to consider
- For social logins, you will need respective social platform key and secret
- For having verified users, you will need an SMTP server with an email address and password using which system can send emails. The system will send a verification link to an email address. Once an email is verified then, only able to access it.
> Note: One can always disable the email verification to allow open sign up, which is not recommended for production as anyone can use anyone's email address 😅
- For persisting user sessions, you will need Redis URL. If you do not configure a Redis server, sessions will be persisted until the instance is up or not restarted. For better response time on authorization requests/middleware, we recommend deploying Redis on the same infra/network as your authorizer server.
- For persisting user sessions, you will need Redis URL (not in case of railway.app). If you do not configure a Redis server, sessions will be persisted until the instance is up or not restarted. For better response time on authorization requests/middleware, we recommend deploying Redis on the same infra/network as your authorizer server.
## Integrating into your website
@@ -197,3 +188,9 @@ This example demonstrates how you can use [`@authorizerdev/authorizer-js`](/auth
onLoad();
</script>
```
---
### Support my work
<a href="https://www.buymeacoffee.com/lakhansamani" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>

View File

@@ -1,5 +1,11 @@
# Task List
## Feature Multiple sessions
- Multiple sessions for users to login use hMset from redis for this
user_id access_token1 long_live_token1
user_id access_token2 long_live_token2
# Feature roles
For the first version we will only support setting roles master list via env

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

985
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,12 +10,13 @@
"author": "Lakhan Samani",
"license": "ISC",
"dependencies": {
"@authorizerdev/authorizer-react": "^0.1.0-beta.19",
"@authorizerdev/authorizer-react": "^0.1.0",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-is": "^17.0.2",
"react-router-dom": "^5.2.0",
"typescript": "^4.3.5"
},

View File

@@ -3,9 +3,11 @@ package constants
var (
ADMIN_SECRET = ""
ENV = ""
ENV_PATH = ""
VERSION = ""
DATABASE_TYPE = ""
DATABASE_URL = ""
DATABASE_NAME = ""
SMTP_HOST = ""
SMTP_PORT = ""
SENDER_EMAIL = ""
@@ -14,13 +16,15 @@ var (
JWT_SECRET = ""
ALLOWED_ORIGINS = []string{}
AUTHORIZER_URL = ""
APP_URL = ""
PORT = "8080"
REDIS_URL = ""
IS_PROD = false
COOKIE_NAME = ""
RESET_PASSWORD_URL = ""
DISABLE_EMAIL_VERIFICATION = "false"
DISABLE_BASIC_AUTHENTICATION = "false"
DISABLE_EMAIL_VERIFICATION = false
DISABLE_BASIC_AUTHENTICATION = false
DISABLE_MAGIC_LOGIN = false
// ROLES
ROLES = []string{}

View File

@@ -2,6 +2,7 @@ package constants
var (
// Ref: https://github.com/qor/auth/blob/master/providers/google/google.go
// deprecated and not used. instead we follow open id approach for google login
GoogleUserInfoURL = "https://www.googleapis.com/oauth2/v3/userinfo"
// Ref: https://github.com/qor/auth/blob/master/providers/facebook/facebook.go#L18
FacebookUserInfoURL = "https://graph.facebook.com/me?fields=id,first_name,last_name,name,email,picture&access_token="

113
server/db/arangodb.go Normal file
View File

@@ -0,0 +1,113 @@
package db
import (
"context"
"log"
"github.com/arangodb/go-driver"
arangoDriver "github.com/arangodb/go-driver"
"github.com/arangodb/go-driver/http"
"github.com/authorizerdev/authorizer/server/constants"
)
// for this we need arangodb instance up and running
// for local testing we can use dockerized version of it
// docker run -p 8529:8529 -e ARANGO_ROOT_PASSWORD=root arangodb/arangodb:3.8.4
func initArangodb() (arangoDriver.Database, error) {
ctx := context.Background()
conn, err := http.NewConnection(http.ConnectionConfig{
Endpoints: []string{constants.DATABASE_URL},
})
if err != nil {
return nil, err
}
client, err := arangoDriver.NewClient(arangoDriver.ClientConfig{
Connection: conn,
})
if err != nil {
return nil, err
}
var arangodb driver.Database
arangodb_exists, err := client.DatabaseExists(nil, constants.DATABASE_NAME)
if arangodb_exists {
log.Println(constants.DATABASE_NAME + " db exists already")
arangodb, err = client.Database(nil, constants.DATABASE_NAME)
if err != nil {
return nil, err
}
} else {
arangodb, err = client.CreateDatabase(nil, constants.DATABASE_NAME, nil)
if err != nil {
return nil, err
}
}
userCollectionExists, err := arangodb.CollectionExists(ctx, Collections.User)
if userCollectionExists {
log.Println(Collections.User + " collection exists already")
} else {
_, err = arangodb.CreateCollection(ctx, Collections.User, nil)
if err != nil {
log.Println("error creating collection("+Collections.User+"):", err)
}
}
userCollection, _ := arangodb.Collection(nil, Collections.User)
userCollection.EnsureHashIndex(ctx, []string{"id"}, &arangoDriver.EnsureHashIndexOptions{
Unique: true,
Sparse: true,
})
userCollection.EnsureHashIndex(ctx, []string{"email"}, &arangoDriver.EnsureHashIndexOptions{
Unique: true,
Sparse: true,
})
verificationRequestCollectionExists, err := arangodb.CollectionExists(ctx, Collections.VerificationRequest)
if verificationRequestCollectionExists {
log.Println(Collections.VerificationRequest + " collection exists already")
} else {
_, err = arangodb.CreateCollection(ctx, Collections.VerificationRequest, nil)
if err != nil {
log.Println("error creating collection("+Collections.VerificationRequest+"):", err)
}
}
verificationRequestCollection, _ := arangodb.Collection(nil, Collections.VerificationRequest)
verificationRequestCollection.EnsureHashIndex(ctx, []string{"id"}, &arangoDriver.EnsureHashIndexOptions{
Unique: true,
Sparse: true,
})
verificationRequestCollection.EnsureHashIndex(ctx, []string{"email", "identifier"}, &arangoDriver.EnsureHashIndexOptions{
Unique: true,
Sparse: true,
})
verificationRequestCollection.EnsureHashIndex(ctx, []string{"token"}, &arangoDriver.EnsureHashIndexOptions{
Unique: true,
Sparse: true,
})
sessionCollectionExists, err := arangodb.CollectionExists(ctx, Collections.Session)
if sessionCollectionExists {
log.Println(Collections.Session + " collection exists already")
} else {
_, err = arangodb.CreateCollection(ctx, Collections.Session, nil)
if err != nil {
log.Println("error creating collection("+Collections.Session+"):", err)
}
}
sessionCollection, _ := arangodb.Collection(nil, Collections.Session)
sessionCollection.EnsureHashIndex(ctx, []string{"id"}, &arangoDriver.EnsureHashIndexOptions{
Unique: true,
Sparse: true,
})
return arangodb, err
}

View File

@@ -3,61 +3,109 @@ package db
import (
"log"
arangoDriver "github.com/arangodb/go-driver"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/google/uuid"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
type Manager interface {
SaveUser(user User) (User, error)
AddUser(user User) (User, error)
UpdateUser(user User) (User, error)
DeleteUser(user User) error
GetUsers() ([]User, error)
GetUserByEmail(email string) (User, error)
GetUserByID(email string) (User, error)
UpdateVerificationTime(verifiedAt int64, id uuid.UUID) error
AddVerification(verification VerificationRequest) (VerificationRequest, error)
GetVerificationByToken(token string) (VerificationRequest, error)
DeleteToken(email string) error
DeleteVerificationRequest(verificationRequest VerificationRequest) error
GetVerificationRequests() ([]VerificationRequest, error)
GetVerificationByEmail(email string) (VerificationRequest, error)
DeleteUser(email string) error
SaveRoles(roles []Role) error
AddSession(session Session) error
}
type manager struct {
db *gorm.DB
sqlDB *gorm.DB
arangodb arangoDriver.Database
}
var Mgr Manager
// mainly used by nosql dbs
type CollectionList struct {
User string
VerificationRequest string
Session string
}
var (
IsSQL bool
IsArangoDB bool
Mgr Manager
Prefix = "authorizer_"
Collections = CollectionList{
User: Prefix + "users",
VerificationRequest: Prefix + "verification_requests",
Session: Prefix + "sessions",
}
)
func InitDB() {
var db *gorm.DB
var sqlDB *gorm.DB
var err error
IsSQL = constants.DATABASE_TYPE != enum.Arangodb.String()
IsArangoDB = constants.DATABASE_TYPE == enum.Arangodb.String()
// sql db orm config
ormConfig := &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "authorizer_",
TablePrefix: Prefix,
},
}
if constants.DATABASE_TYPE == enum.Postgres.String() {
db, err = gorm.Open(postgres.Open(constants.DATABASE_URL), ormConfig)
}
if constants.DATABASE_TYPE == enum.Mysql.String() {
db, err = gorm.Open(mysql.Open(constants.DATABASE_URL), ormConfig)
}
if constants.DATABASE_TYPE == enum.Sqlite.String() {
db, err = gorm.Open(sqlite.Open(constants.DATABASE_URL), ormConfig)
}
log.Println("db type:", constants.DATABASE_TYPE)
switch constants.DATABASE_TYPE {
case enum.Postgres.String():
sqlDB, err = gorm.Open(postgres.Open(constants.DATABASE_URL), ormConfig)
break
case enum.Sqlite.String():
sqlDB, err = gorm.Open(sqlite.Open(constants.DATABASE_URL), ormConfig)
break
case enum.Mysql.String():
sqlDB, err = gorm.Open(mysql.Open(constants.DATABASE_URL), ormConfig)
break
case enum.SQLServer.String():
sqlDB, err = gorm.Open(sqlserver.Open(constants.DATABASE_URL), ormConfig)
break
case enum.Arangodb.String():
arangodb, err := initArangodb()
if err != nil {
log.Fatal("Failed to init db:", err)
} else {
db.AutoMigrate(&User{}, &VerificationRequest{}, &Role{})
log.Fatal("error initing arangodb:", err)
}
Mgr = &manager{db: db}
Mgr = &manager{
sqlDB: nil,
arangodb: arangodb,
}
break
}
// common for all sql dbs that are configured via gorm
if IsSQL {
if err != nil {
log.Fatal("Failed to init sqlDB:", err)
} else {
sqlDB.AutoMigrate(&User{}, &VerificationRequest{}, &Session{})
}
Mgr = &manager{
sqlDB: sqlDB,
arangodb: nil,
}
}
}

View File

@@ -1,34 +0,0 @@
package db
import (
"log"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Role struct {
ID uuid.UUID `gorm:"type:uuid;"`
Role string `gorm:"unique"`
}
func (r *Role) BeforeCreate(tx *gorm.DB) (err error) {
r.ID = uuid.New()
return
}
// SaveRoles function to save roles
func (mgr *manager) SaveRoles(roles []Role) error {
res := mgr.db.Clauses(
clause.OnConflict{
DoNothing: true,
}).Create(&roles)
if res.Error != nil {
log.Println(`Error saving roles`)
return res.Error
}
return nil
}

64
server/db/session.go Normal file
View File

@@ -0,0 +1,64 @@
package db
import (
"log"
"time"
"github.com/google/uuid"
"gorm.io/gorm/clause"
)
type Session struct {
Key string `json:"_key,omitempty"` // for arangodb
ObjectID string `json:"_id,omitempty"` // for arangodb & mongodb
ID string `gorm:"primaryKey;type:char(36)" json:"id"`
UserID string `gorm:"type:char(36)" json:"user_id"`
User User `json:"-"`
UserAgent string `json:"user_agent"`
IP string `json:"ip"`
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at"`
}
// AddSession function to save user sessiosn
func (mgr *manager) AddSession(session Session) error {
if session.ID == "" {
session.ID = uuid.New().String()
}
if session.CreatedAt == 0 {
session.CreatedAt = time.Now().Unix()
}
if session.UpdatedAt == 0 {
session.CreatedAt = time.Now().Unix()
}
if IsSQL {
// copy id as value for fields required for mongodb & arangodb
session.Key = session.ID
session.ObjectID = session.ID
res := mgr.sqlDB.Clauses(
clause.OnConflict{
DoNothing: true,
}).Create(&session)
if res.Error != nil {
log.Println(`error saving session`, res.Error)
return res.Error
}
}
if IsArangoDB {
session.CreatedAt = time.Now().Unix()
session.UpdatedAt = time.Now().Unix()
sessionCollection, _ := mgr.arangodb.Collection(nil, Collections.Session)
_, err := sessionCollection.CreateDocument(nil, session)
if err != nil {
log.Println(`error saving session`, err)
return err
}
}
return nil
}

View File

@@ -1,118 +1,243 @@
package db
import (
"context"
"errors"
"fmt"
"log"
"time"
"github.com/arangodb/go-driver"
arangoDriver "github.com/arangodb/go-driver"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type User struct {
ID uuid.UUID `gorm:"type:uuid;"`
FirstName string
LastName string
Email string `gorm:"unique"`
Password string
SignupMethod string
EmailVerifiedAt int64
CreatedAt int64 `gorm:"autoCreateTime"`
UpdatedAt int64 `gorm:"autoUpdateTime"`
Image string
Roles string
Key string `json:"_key,omitempty"` // for arangodb
ObjectID string `json:"_id,omitempty"` // for arangodb & mongodb
ID string `gorm:"primaryKey;type:char(36)" json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `gorm:"unique" json:"email"`
Password string `gorm:"type:text" json:"password"`
SignupMethod string `json:"signup_method"`
EmailVerifiedAt int64 `json:"email_verified_at"`
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at"`
Image string `gorm:"type:text" json:"image"`
Roles string `json:"roles"`
}
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
u.ID = uuid.New()
// AddUser function to add user even with email conflict
func (mgr *manager) AddUser(user User) (User, error) {
if user.ID == "" {
user.ID = uuid.New().String()
}
return
}
// SaveUser function to add user even with email conflict
func (mgr *manager) SaveUser(user User) (User, error) {
result := mgr.db.Clauses(
if IsSQL {
// copy id as value for fields required for mongodb & arangodb
user.Key = user.ID
user.ObjectID = user.ID
result := mgr.sqlDB.Clauses(
clause.OnConflict{
UpdateAll: true,
Columns: []clause.Column{{Name: "email"}},
}).Create(&user)
if result.Error != nil {
log.Println(result.Error)
log.Println("error adding user:", result.Error)
return user, result.Error
}
}
if IsArangoDB {
user.CreatedAt = time.Now().Unix()
user.UpdatedAt = time.Now().Unix()
ctx := context.Background()
userCollection, _ := mgr.arangodb.Collection(nil, Collections.User)
meta, err := userCollection.CreateDocument(arangoDriver.WithOverwrite(ctx), user)
if err != nil {
log.Println("error adding user:", err)
return user, err
}
user.Key = meta.Key
user.ObjectID = meta.ID.String()
}
return user, nil
}
// UpdateUser function to update user with ID conflict
func (mgr *manager) UpdateUser(user User) (User, error) {
user.UpdatedAt = time.Now().Unix()
result := mgr.db.Clauses(
clause.OnConflict{
UpdateAll: true,
Columns: []clause.Column{{Name: "email"}},
}).Create(&user)
if IsSQL {
result := mgr.sqlDB.Save(&user)
if result.Error != nil {
log.Println(result.Error)
log.Println("error updating user:", result.Error)
return user, result.Error
}
}
if IsArangoDB {
collection, _ := mgr.arangodb.Collection(nil, Collections.User)
meta, err := collection.UpdateDocument(nil, user.Key, user)
if err != nil {
log.Println("error updating user:", err)
return user, err
}
user.Key = meta.Key
user.ObjectID = meta.ID.String()
}
return user, nil
}
// GetUsers function to get all users
func (mgr *manager) GetUsers() ([]User, error) {
var users []User
result := mgr.db.Find(&users)
if IsSQL {
result := mgr.sqlDB.Find(&users)
if result.Error != nil {
log.Println(result.Error)
log.Println("error getting users:", result.Error)
return users, result.Error
}
}
if IsArangoDB {
query := fmt.Sprintf("FOR d in %s RETURN d", Collections.User)
cursor, err := mgr.arangodb.Query(nil, query, nil)
if err != nil {
return users, err
}
defer cursor.Close()
for {
var user User
meta, err := cursor.ReadDocument(nil, &user)
if driver.IsNoMoreDocuments(err) {
break
} else if err != nil {
return users, err
}
if meta.Key != "" {
user.Key = meta.Key
user.ObjectID = meta.ID.String()
users = append(users, user)
}
}
}
return users, nil
}
func (mgr *manager) GetUserByEmail(email string) (User, error) {
var user User
result := mgr.db.Where("email = ?", email).First(&user)
if IsSQL {
result := mgr.sqlDB.Where("email = ?", email).First(&user)
if result.Error != nil {
return user, result.Error
}
}
if IsArangoDB {
query := fmt.Sprintf("FOR d in %s FILTER d.email == @email RETURN d", Collections.User)
bindVars := map[string]interface{}{
"email": email,
}
cursor, err := mgr.arangodb.Query(nil, query, bindVars)
if err != nil {
return user, err
}
defer cursor.Close()
for {
if !cursor.HasMore() {
if user.Key == "" {
return user, fmt.Errorf("user not found")
}
break
}
_, err := cursor.ReadDocument(nil, &user)
if err != nil {
return user, err
}
}
}
return user, nil
}
func (mgr *manager) GetUserByID(id string) (User, error) {
var user User
result := mgr.db.Where("id = ?", id).First(&user)
if IsSQL {
result := mgr.sqlDB.Where("id = ?", id).First(&user)
if result.Error != nil {
return user, result.Error
}
}
if IsArangoDB {
query := fmt.Sprintf("FOR d in %s FILTER d.id == @id LIMIT 1 RETURN d", Collections.User)
bindVars := map[string]interface{}{
"id": id,
}
cursor, err := mgr.arangodb.Query(nil, query, bindVars)
if err != nil {
return user, err
}
defer cursor.Close()
count := cursor.Count()
if count == 0 {
return user, errors.New("user not found")
}
for {
if !cursor.HasMore() {
if user.Key == "" {
return user, fmt.Errorf("user not found")
}
break
}
_, err := cursor.ReadDocument(nil, &user)
if err != nil {
return user, err
}
}
}
return user, nil
}
func (mgr *manager) UpdateVerificationTime(verifiedAt int64, id uuid.UUID) error {
user := &User{
ID: id,
}
result := mgr.db.Model(&user).Where("id = ?", id).Update("email_verified_at", verifiedAt)
func (mgr *manager) DeleteUser(user User) error {
if IsSQL {
result := mgr.sqlDB.Delete(&user)
if result.Error != nil {
log.Println(`error deleting user:`, result.Error)
return result.Error
}
return nil
}
func (mgr *manager) DeleteUser(email string) error {
var user User
result := mgr.db.Where("email = ?", email).Delete(&user)
if result.Error != nil {
log.Println(`Error deleting user:`, result.Error)
return result.Error
}
}
if IsArangoDB {
collection, _ := mgr.arangodb.Collection(nil, Collections.User)
_, err := collection.RemoveDocument(nil, user.Key)
if err != nil {
log.Println(`error deleting user:`, err)
return err
}
}
return nil

View File

@@ -1,85 +1,199 @@
package db
import (
"fmt"
"log"
"github.com/arangodb/go-driver"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type VerificationRequest struct {
ID uuid.UUID `gorm:"type:uuid;"`
Token string `gorm:"index"`
Identifier string
ExpiresAt int64
CreatedAt int64 `gorm:"autoCreateTime"`
UpdatedAt int64 `gorm:"autoUpdateTime"`
Email string `gorm:"unique"`
}
func (v *VerificationRequest) BeforeCreate(tx *gorm.DB) (err error) {
v.ID = uuid.New()
return
Key string `json:"_key,omitempty"` // for arangodb
ObjectID string `json:"_id,omitempty"` // for arangodb & mongodb
ID string `gorm:"primaryKey;type:char(36)" json:"id"`
Token string `gorm:"type:text" json:"token"`
Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier"`
ExpiresAt int64 `json:"expires_at"`
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at"`
Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email"`
}
// AddVerification function to add verification record
func (mgr *manager) AddVerification(verification VerificationRequest) (VerificationRequest, error) {
result := mgr.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}},
DoUpdates: clause.AssignmentColumns([]string{"token", "identifier", "expires_at"}),
if verification.ID == "" {
verification.ID = uuid.New().String()
}
if IsSQL {
// copy id as value for fields required for mongodb & arangodb
verification.Key = verification.ID
verification.ObjectID = verification.ID
result := mgr.sqlDB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}, {Name: "identifier"}},
DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at"}),
}).Create(&verification)
if result.Error != nil {
log.Println(`Error saving verification record`, result.Error)
log.Println(`error saving verification record`, result.Error)
return verification, result.Error
}
}
if IsArangoDB {
verificationRequestCollection, _ := mgr.arangodb.Collection(nil, Collections.VerificationRequest)
meta, err := verificationRequestCollection.CreateDocument(nil, verification)
if err != nil {
return verification, err
}
verification.Key = meta.Key
verification.ObjectID = meta.ID.String()
}
return verification, nil
}
// GetVerificationRequests function to get all verification requests
func (mgr *manager) GetVerificationRequests() ([]VerificationRequest, error) {
var verificationRequests []VerificationRequest
if IsSQL {
result := mgr.sqlDB.Find(&verificationRequests)
if result.Error != nil {
log.Println("error getting verification requests:", result.Error)
return verificationRequests, result.Error
}
}
if IsArangoDB {
query := fmt.Sprintf("FOR d in %s RETURN d", Collections.VerificationRequest)
cursor, err := mgr.arangodb.Query(nil, query, nil)
if err != nil {
return verificationRequests, err
}
defer cursor.Close()
for {
var verificationRequest VerificationRequest
meta, err := cursor.ReadDocument(nil, &verificationRequest)
if driver.IsNoMoreDocuments(err) {
break
} else if err != nil {
return verificationRequests, err
}
if meta.Key != "" {
verificationRequest.Key = meta.Key
verificationRequest.ObjectID = meta.ID.String()
verificationRequests = append(verificationRequests, verificationRequest)
}
}
}
return verificationRequests, nil
}
func (mgr *manager) GetVerificationByToken(token string) (VerificationRequest, error) {
var verification VerificationRequest
result := mgr.db.Where("token = ?", token).First(&verification)
if IsSQL {
result := mgr.sqlDB.Where("token = ?", token).First(&verification)
if result.Error != nil {
log.Println(`Error getting verification token:`, result.Error)
log.Println(`error getting verification request:`, result.Error)
return verification, result.Error
}
}
if IsArangoDB {
query := fmt.Sprintf("FOR d in %s FILTER d.token == @token LIMIT 1 RETURN d", Collections.VerificationRequest)
bindVars := map[string]interface{}{
"token": token,
}
cursor, err := mgr.arangodb.Query(nil, query, bindVars)
if err != nil {
return verification, err
}
defer cursor.Close()
for {
if !cursor.HasMore() {
if verification.Key == "" {
return verification, fmt.Errorf("verification request not found")
}
break
}
_, err := cursor.ReadDocument(nil, &verification)
if err != nil {
return verification, err
}
}
}
return verification, nil
}
func (mgr *manager) GetVerificationByEmail(email string) (VerificationRequest, error) {
var verification VerificationRequest
result := mgr.db.Where("email = ?", email).First(&verification)
if IsSQL {
result := mgr.sqlDB.Where("email = ?", email).First(&verification)
if result.Error != nil {
log.Println(`Error getting verification token:`, result.Error)
log.Println(`error getting verification token:`, result.Error)
return verification, result.Error
}
}
if IsArangoDB {
query := fmt.Sprintf("FOR d in %s FILTER d.email == @email LIMIT 1 RETURN d", Collections.VerificationRequest)
bindVars := map[string]interface{}{
"email": email,
}
cursor, err := mgr.arangodb.Query(nil, query, bindVars)
if err != nil {
return verification, err
}
defer cursor.Close()
for {
if !cursor.HasMore() {
if verification.Key == "" {
return verification, fmt.Errorf("verification request not found")
}
break
}
_, err := cursor.ReadDocument(nil, &verification)
if err != nil {
return verification, err
}
}
}
return verification, nil
}
func (mgr *manager) DeleteToken(email string) error {
var verification VerificationRequest
result := mgr.db.Where("email = ?", email).Delete(&verification)
func (mgr *manager) DeleteVerificationRequest(verificationRequest VerificationRequest) error {
if IsSQL {
result := mgr.sqlDB.Delete(&verificationRequest)
if result.Error != nil {
log.Println(`Error deleting token:`, result.Error)
log.Println(`error deleting verification request:`, result.Error)
return result.Error
}
}
if IsArangoDB {
collection, _ := mgr.arangodb.Collection(nil, Collections.VerificationRequest)
_, err := collection.RemoveDocument(nil, verificationRequest.Key)
if err != nil {
log.Println(`error deleting verification request:`, err)
return err
}
}
return nil
}
// GetUsers function to get all users
func (mgr *manager) GetVerificationRequests() ([]VerificationRequest, error) {
var verificationRequests []VerificationRequest
result := mgr.db.Find(&verificationRequests)
if result.Error != nil {
log.Println(result.Error)
return verificationRequests, result.Error
}
return verificationRequests, nil
}

View File

@@ -6,6 +6,8 @@ const (
Postgres DbType = iota
Sqlite
Mysql
SQLServer
Arangodb
)
func (d DbType) String() string {
@@ -13,5 +15,7 @@ func (d DbType) String() string {
"postgres",
"sqlite",
"mysql",
"sqlserver",
"arangodb",
}[d]
}

View File

@@ -1,4 +1,4 @@
package main
package env
import (
"flag"
@@ -13,7 +13,7 @@ import (
// build variables
var (
Version string
VERSION string
ARG_DB_URL *string
ARG_DB_TYPE *string
ARG_AUTHORIZER_URL *string
@@ -22,7 +22,9 @@ var (
// InitEnv -> to initialize env and through error if required env are not present
func InitEnv() {
envPath := `.env`
if constants.ENV_PATH == "" {
constants.ENV_PATH = `.env`
}
ARG_DB_URL = flag.String("database_url", "", "Database connection string")
ARG_DB_TYPE = flag.String("database_type", "", "Database type, possible values are postgres,mysql,sqlite")
ARG_AUTHORIZER_URL = flag.String("authorizer_url", "", "URL for authorizer instance, eg: https://xyz.herokuapp.com")
@@ -30,19 +32,20 @@ func InitEnv() {
flag.Parse()
if *ARG_ENV_FILE != "" {
envPath = *ARG_ENV_FILE
constants.ENV_PATH = *ARG_ENV_FILE
}
err := godotenv.Load(envPath)
err := godotenv.Load(constants.ENV_PATH)
if err != nil {
log.Println("Error loading .env file")
log.Printf("error loading %s file", constants.ENV_PATH)
}
constants.VERSION = Version
constants.VERSION = VERSION
constants.ADMIN_SECRET = os.Getenv("ADMIN_SECRET")
constants.ENV = os.Getenv("ENV")
constants.DATABASE_TYPE = os.Getenv("DATABASE_TYPE")
constants.DATABASE_URL = os.Getenv("DATABASE_URL")
constants.DATABASE_NAME = os.Getenv("DATABASE_NAME")
constants.SMTP_HOST = os.Getenv("SMTP_HOST")
constants.SMTP_PORT = os.Getenv("SMTP_PORT")
constants.SENDER_EMAIL = os.Getenv("SENDER_EMAIL")
@@ -62,8 +65,9 @@ func InitEnv() {
constants.TWITTER_CLIENT_ID = os.Getenv("TWITTER_CLIENT_ID")
constants.TWITTER_CLIENT_SECRET = os.Getenv("TWITTER_CLIENT_SECRET")
constants.RESET_PASSWORD_URL = strings.TrimPrefix(os.Getenv("RESET_PASSWORD_URL"), "/")
constants.DISABLE_BASIC_AUTHENTICATION = os.Getenv("DISABLE_BASIC_AUTHENTICATION")
constants.DISABLE_EMAIL_VERIFICATION = os.Getenv("DISABLE_EMAIL_VERIFICATION")
constants.DISABLE_BASIC_AUTHENTICATION = os.Getenv("DISABLE_BASIC_AUTHENTICATION") == "true"
constants.DISABLE_EMAIL_VERIFICATION = os.Getenv("DISABLE_EMAIL_VERIFICATION") == "true"
constants.DISABLE_MAGIC_LOGIN = os.Getenv("DISABLE_MAGIC_LOGIN") == "true"
constants.JWT_ROLE_CLAIM = os.Getenv("JWT_ROLE_CLAIM")
if constants.ADMIN_SECRET == "" {
@@ -114,6 +118,10 @@ func InitEnv() {
panic("Database type is required")
}
if constants.DATABASE_NAME == "" {
constants.DATABASE_NAME = "authorizer"
}
if constants.JWT_TYPE == "" {
constants.JWT_TYPE = "HS256"
}
@@ -122,16 +130,13 @@ func InitEnv() {
constants.COOKIE_NAME = "authorizer"
}
if constants.DISABLE_BASIC_AUTHENTICATION == "" {
constants.DISABLE_BASIC_AUTHENTICATION = "false"
if constants.SMTP_HOST == "" || constants.SENDER_EMAIL == "" || constants.SENDER_PASSWORD == "" {
constants.DISABLE_EMAIL_VERIFICATION = true
constants.DISABLE_MAGIC_LOGIN = true
}
if constants.DISABLE_EMAIL_VERIFICATION == "" && constants.DISABLE_BASIC_AUTHENTICATION == "false" {
if constants.SMTP_HOST == "" || constants.SENDER_EMAIL == "" || constants.SENDER_PASSWORD == "" {
constants.DISABLE_EMAIL_VERIFICATION = "true"
} else {
constants.DISABLE_EMAIL_VERIFICATION = "false"
}
if constants.DISABLE_EMAIL_VERIFICATION {
constants.DISABLE_MAGIC_LOGIN = true
}
rolesSplit := strings.Split(os.Getenv("ROLES"), ",")

29
server/env/env_test.go vendored Normal file
View File

@@ -0,0 +1,29 @@
package env
import (
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/stretchr/testify/assert"
)
func TestEnvs(t *testing.T) {
constants.ENV_PATH = "../../.env.sample"
InitEnv()
assert.Equal(t, constants.ADMIN_SECRET, "admin")
assert.Equal(t, constants.ENV, "production")
assert.Equal(t, constants.DATABASE_URL, "data.db")
assert.Equal(t, constants.DATABASE_TYPE, enum.Sqlite.String())
assert.True(t, constants.DISABLE_EMAIL_VERIFICATION)
assert.True(t, constants.DISABLE_MAGIC_LOGIN)
assert.False(t, constants.DISABLE_BASIC_AUTHENTICATION)
assert.Equal(t, constants.JWT_TYPE, "HS256")
assert.Equal(t, constants.JWT_SECRET, "random_string")
assert.Equal(t, constants.JWT_ROLE_CLAIM, "role")
assert.EqualValues(t, constants.ROLES, []string{"user"})
assert.EqualValues(t, constants.DEFAULT_ROLES, []string{"user"})
assert.EqualValues(t, constants.PROTECTED_ROLES, []string{"admin"})
assert.EqualValues(t, constants.ALLOWED_ORIGINS, []string{"*"})
}

View File

@@ -4,29 +4,34 @@ go 1.16
require (
github.com/99designs/gqlgen v0.13.0
github.com/gin-contrib/location v0.0.2 // indirect
github.com/arangodb/go-driver v1.2.1
github.com/coreos/go-oidc/v3 v3.1.0
github.com/gin-contrib/location v0.0.2
github.com/gin-gonic/gin v1.7.2
github.com/go-playground/validator/v10 v10.8.0 // indirect
github.com/go-redis/redis/v8 v8.11.0
github.com/golang-jwt/jwt v3.2.1+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0
github.com/jackc/pgproto3/v2 v2.1.0 // indirect
github.com/joho/godotenv v1.3.0
github.com/json-iterator/go v1.1.11 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-sqlite3 v1.14.7 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f
github.com/stretchr/testify v1.7.0 // indirect
github.com/ugorji/go v1.2.6 // indirect
github.com/vektah/gqlparser/v2 v2.1.0
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gorm.io/driver/mysql v1.1.1
gorm.io/driver/postgres v1.1.0
gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.21.11
rogchap.com/v8go v0.6.0 // indirect
gorm.io/driver/mysql v1.2.1
gorm.io/driver/postgres v1.2.3
gorm.io/driver/sqlite v1.2.6
gorm.io/driver/sqlserver v1.2.1
gorm.io/gorm v1.22.4
)

File diff suppressed because it is too large Load Diff

View File

@@ -61,6 +61,7 @@ type ComplexityRoot struct {
IsFacebookLoginEnabled func(childComplexity int) int
IsGithubLoginEnabled func(childComplexity int) int
IsGoogleLoginEnabled func(childComplexity int) int
IsMagicLoginEnabled func(childComplexity int) int
IsTwitterLoginEnabled func(childComplexity int) int
Version func(childComplexity int) int
}
@@ -71,6 +72,7 @@ type ComplexityRoot struct {
ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int
Login func(childComplexity int, params model.LoginInput) int
Logout func(childComplexity int) int
MagicLogin func(childComplexity int, params model.MagicLoginInput) int
ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int
ResetPassword func(childComplexity int, params model.ResetPasswordInput) int
Signup func(childComplexity int, params model.SignUpInput) int
@@ -117,6 +119,7 @@ type ComplexityRoot struct {
type MutationResolver interface {
Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse, error)
Login(ctx context.Context, params model.LoginInput) (*model.AuthResponse, error)
MagicLogin(ctx context.Context, params model.MagicLoginInput) (*model.Response, error)
Logout(ctx context.Context) (*model.Response, error)
UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model.Response, error)
AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*model.User, error)
@@ -226,6 +229,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Meta.IsGoogleLoginEnabled(childComplexity), true
case "Meta.isMagicLoginEnabled":
if e.complexity.Meta.IsMagicLoginEnabled == nil {
break
}
return e.complexity.Meta.IsMagicLoginEnabled(childComplexity), true
case "Meta.isTwitterLoginEnabled":
if e.complexity.Meta.IsTwitterLoginEnabled == nil {
break
@@ -295,6 +305,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.Logout(childComplexity), true
case "Mutation.magicLogin":
if e.complexity.Mutation.MagicLogin == nil {
break
}
args, err := ec.field_Mutation_magicLogin_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.MagicLogin(childComplexity, args["params"].(model.MagicLoginInput)), true
case "Mutation.resendVerifyEmail":
if e.complexity.Mutation.ResendVerifyEmail == nil {
break
@@ -600,6 +622,7 @@ type Meta {
isGithubLoginEnabled: Boolean!
isEmailVerificationEnabled: Boolean!
isBasicAuthenticationEnabled: Boolean!
isMagicLoginEnabled: Boolean!
}
type User {
@@ -699,9 +722,15 @@ input DeleteUserInput {
email: String!
}
input MagicLoginInput {
email: String!
roles: [String!]
}
type Mutation {
signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse!
magicLogin(params: MagicLoginInput!): Response!
logout: Response!
updateProfile(params: UpdateProfileInput!): Response!
adminUpdateUser(params: AdminUpdateUserInput!): User!
@@ -787,6 +816,21 @@ func (ec *executionContext) field_Mutation_login_args(ctx context.Context, rawAr
return args, nil
}
func (ec *executionContext) field_Mutation_magicLogin_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 model.MagicLoginInput
if tmp, ok := rawArgs["params"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params"))
arg0, err = ec.unmarshalNMagicLoginInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐMagicLoginInput(ctx, tmp)
if err != nil {
return nil, err
}
}
args["params"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_resendVerifyEmail_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@@ -1376,6 +1420,41 @@ func (ec *executionContext) _Meta_isBasicAuthenticationEnabled(ctx context.Conte
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _Meta_isMagicLoginEnabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Meta",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.IsMagicLoginEnabled, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_signup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -1460,6 +1539,48 @@ func (ec *executionContext) _Mutation_login(ctx context.Context, field graphql.C
return ec.marshalNAuthResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAuthResponse(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_magicLogin(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_magicLogin_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().MagicLogin(rctx, args["params"].(model.MagicLoginInput))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*model.Response)
fc.Result = res
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_logout(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -3856,6 +3977,34 @@ func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj in
return it, nil
}
func (ec *executionContext) unmarshalInputMagicLoginInput(ctx context.Context, obj interface{}) (model.MagicLoginInput, error) {
var it model.MagicLoginInput
var asMap = obj.(map[string]interface{})
for k, v := range asMap {
switch k {
case "email":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email"))
it.Email, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
case "roles":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("roles"))
it.Roles, err = ec.unmarshalOString2ᚕstringᚄ(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputResendVerifyEmailInput(ctx context.Context, obj interface{}) (model.ResendVerifyEmailInput, error) {
var it model.ResendVerifyEmailInput
var asMap = obj.(map[string]interface{})
@@ -4187,6 +4336,11 @@ func (ec *executionContext) _Meta(ctx context.Context, sel ast.SelectionSet, obj
if out.Values[i] == graphql.Null {
invalids++
}
case "isMagicLoginEnabled":
out.Values[i] = ec._Meta_isMagicLoginEnabled(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@@ -4223,6 +4377,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
invalids++
}
case "magicLogin":
out.Values[i] = ec._Mutation_magicLogin(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "logout":
out.Values[i] = ec._Mutation_logout(ctx, field)
if out.Values[i] == graphql.Null {
@@ -4800,6 +4959,11 @@ func (ec *executionContext) unmarshalNLoginInput2githubᚗcomᚋauthorizerdevᚋ
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) unmarshalNMagicLoginInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐMagicLoginInput(ctx context.Context, v interface{}) (model.MagicLoginInput, error) {
res, err := ec.unmarshalInputMagicLoginInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNMeta2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐMeta(ctx context.Context, sel ast.SelectionSet, v model.Meta) graphql.Marshaler {
return ec._Meta(ctx, sel, &v)
}

View File

@@ -37,6 +37,11 @@ type LoginInput struct {
Roles []string `json:"roles"`
}
type MagicLoginInput struct {
Email string `json:"email"`
Roles []string `json:"roles"`
}
type Meta struct {
Version string `json:"version"`
IsGoogleLoginEnabled bool `json:"isGoogleLoginEnabled"`
@@ -45,6 +50,7 @@ type Meta struct {
IsGithubLoginEnabled bool `json:"isGithubLoginEnabled"`
IsEmailVerificationEnabled bool `json:"isEmailVerificationEnabled"`
IsBasicAuthenticationEnabled bool `json:"isBasicAuthenticationEnabled"`
IsMagicLoginEnabled bool `json:"isMagicLoginEnabled"`
}
type ResendVerifyEmailInput struct {

View File

@@ -13,6 +13,7 @@ type Meta {
isGithubLoginEnabled: Boolean!
isEmailVerificationEnabled: Boolean!
isBasicAuthenticationEnabled: Boolean!
isMagicLoginEnabled: Boolean!
}
type User {
@@ -112,9 +113,15 @@ input DeleteUserInput {
email: String!
}
input MagicLoginInput {
email: String!
roles: [String!]
}
type Mutation {
signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse!
magicLogin(params: MagicLoginInput!): Response!
logout: Response!
updateProfile(params: UpdateProfileInput!): Response!
adminUpdateUser(params: AdminUpdateUserInput!): User!

View File

@@ -19,6 +19,10 @@ func (r *mutationResolver) Login(ctx context.Context, params model.LoginInput) (
return resolvers.Login(ctx, params)
}
func (r *mutationResolver) MagicLogin(ctx context.Context, params model.MagicLoginInput) (*model.Response, error) {
return resolvers.MagicLogin(ctx, params)
}
func (r *mutationResolver) Logout(ctx context.Context) (*model.Response, error) {
return resolvers.Logout(ctx)
}

View File

@@ -1,6 +1,7 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
@@ -15,36 +16,50 @@ import (
"github.com/authorizerdev/authorizer/server/oauth"
"github.com/authorizerdev/authorizer/server/session"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"
)
func processGoogleUserInfo(code string) (db.User, error) {
user := db.User{}
token, err := oauth.OAuthProvider.GoogleConfig.Exchange(oauth2.NoContext, code)
ctx := context.Background()
oauth2Token, err := oauth.OAuthProviders.GoogleConfig.Exchange(ctx, code)
if err != nil {
return user, fmt.Errorf("invalid google exchange code: %s", err.Error())
}
client := oauth.OAuthProvider.GoogleConfig.Client(oauth2.NoContext, token)
response, err := client.Get(constants.GoogleUserInfoURL)
if err != nil {
return user, err
verifier := oauth.OIDCProviders.GoogleOIDC.Verifier(&oidc.Config{ClientID: oauth.OAuthProviders.GoogleConfig.ClientID})
// Extract the ID Token from OAuth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
return user, fmt.Errorf("unable to extract id_token")
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
// Parse and verify ID Token payload.
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
return user, fmt.Errorf("failed to read google response body: %s", err.Error())
return user, fmt.Errorf("unable to verify id_token: %s", err.Error())
}
userRawData := make(map[string]string)
json.Unmarshal(body, &userRawData)
// Extract custom claims
var claims struct {
Email string `json:"email"`
Picture string `json:"picture"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Verified bool `json:"email_verified"`
}
if err := idToken.Claims(&claims); err != nil {
return user, fmt.Errorf("unable to extract claims")
}
user = db.User{
FirstName: userRawData["given_name"],
LastName: userRawData["family_name"],
Image: userRawData["picture"],
Email: userRawData["email"],
FirstName: claims.GivenName,
LastName: claims.FamilyName,
Image: claims.Picture,
Email: claims.Email,
EmailVerifiedAt: time.Now().Unix(),
}
@@ -53,7 +68,7 @@ func processGoogleUserInfo(code string) (db.User, error) {
func processGithubUserInfo(code string) (db.User, error) {
user := db.User{}
token, err := oauth.OAuthProvider.GithubConfig.Exchange(oauth2.NoContext, code)
token, err := oauth.OAuthProviders.GithubConfig.Exchange(oauth2.NoContext, code)
if err != nil {
return user, fmt.Errorf("invalid github exchange code: %s", err.Error())
}
@@ -102,7 +117,7 @@ func processGithubUserInfo(code string) (db.User, error) {
func processFacebookUserInfo(code string) (db.User, error) {
user := db.User{}
token, err := oauth.OAuthProvider.FacebookConfig.Exchange(oauth2.NoContext, code)
token, err := oauth.OAuthProviders.FacebookConfig.Exchange(oauth2.NoContext, code)
if err != nil {
return user, fmt.Errorf("invalid facebook exchange code: %s", err.Error())
}
@@ -114,7 +129,7 @@ func processFacebookUserInfo(code string) (db.User, error) {
response, err := client.Do(req)
if err != nil {
log.Println("err:", err)
log.Println("error processing facebook user info:", err)
return user, err
}
@@ -147,11 +162,11 @@ func OAuthCallbackHandler() gin.HandlerFunc {
provider := c.Param("oauth_provider")
state := c.Request.FormValue("state")
sessionState := session.GetToken(state)
sessionState := session.GetSocailLoginState(state)
if sessionState == "" {
c.JSON(400, gin.H{"error": "invalid oauth state"})
}
session.DeleteToken(sessionState)
session.RemoveSocialLoginState(state)
// contains random token, redirect url, role
sessionSplit := strings.Split(state, "___")
@@ -202,13 +217,14 @@ func OAuthCallbackHandler() gin.HandlerFunc {
}
user.Roles = strings.Join(inputRoles, ",")
user, _ = db.Mgr.AddUser(user)
} else {
// user exists in db, check if method was google
// if not append google to existing signup method and save it
signupMethod := existingUser.SignupMethod
if !strings.Contains(signupMethod, provider) {
signupMethod = signupMethod + "," + enum.Github.String()
signupMethod = signupMethod + "," + provider
}
user.SignupMethod = signupMethod
user.Password = existingUser.Password
@@ -245,16 +261,28 @@ func OAuthCallbackHandler() gin.HandlerFunc {
} else {
user.Roles = existingUser.Roles
}
user.Key = existingUser.Key
user.ObjectID = existingUser.ObjectID
user.ID = existingUser.ID
user, err = db.Mgr.UpdateUser(user)
}
user, _ = db.Mgr.SaveUser(user)
user, _ = db.Mgr.GetUserByEmail(user.Email)
userIdStr := fmt.Sprintf("%v", user.ID)
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, inputRoles)
accessToken, _, _ := utils.CreateAuthToken(user, enum.AccessToken, inputRoles)
utils.SetCookie(c, accessToken)
session.SetToken(userIdStr, refreshToken)
session.SetToken(userIdStr, accessToken, refreshToken)
go func() {
sessionData := db.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(c.Request),
IP: utils.GetIP(c.Request),
}
db.Mgr.AddSession(sessionData)
}()
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
}

View File

@@ -48,28 +48,46 @@ func OAuthLoginHandler() gin.HandlerFunc {
oauthStateString := uuid.String() + "___" + redirectURL + "___" + roles
provider := c.Param("oauth_provider")
isProviderConfigured := true
switch provider {
case enum.Google.String():
session.SetToken(oauthStateString, enum.Google.String())
if oauth.OAuthProviders.GoogleConfig == nil {
isProviderConfigured = false
break
}
session.SetSocailLoginState(oauthStateString, enum.Google.String())
// during the init of OAuthProvider authorizer url might be empty
oauth.OAuthProvider.GoogleConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/google"
url := oauth.OAuthProvider.GoogleConfig.AuthCodeURL(oauthStateString)
oauth.OAuthProviders.GoogleConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/google"
url := oauth.OAuthProviders.GoogleConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url)
case enum.Github.String():
session.SetToken(oauthStateString, enum.Github.String())
oauth.OAuthProvider.GithubConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/github"
url := oauth.OAuthProvider.GithubConfig.AuthCodeURL(oauthStateString)
if oauth.OAuthProviders.GithubConfig == nil {
isProviderConfigured = false
break
}
session.SetSocailLoginState(oauthStateString, enum.Github.String())
oauth.OAuthProviders.GithubConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/github"
url := oauth.OAuthProviders.GithubConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url)
case enum.Facebook.String():
session.SetToken(oauthStateString, enum.Github.String())
oauth.OAuthProvider.FacebookConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/facebook"
url := oauth.OAuthProvider.FacebookConfig.AuthCodeURL(oauthStateString)
if oauth.OAuthProviders.FacebookConfig == nil {
isProviderConfigured = false
break
}
session.SetSocailLoginState(oauthStateString, enum.Facebook.String())
oauth.OAuthProviders.FacebookConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/facebook"
url := oauth.OAuthProviders.FacebookConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url)
default:
c.JSON(422, gin.H{
"message": "Invalid oauth provider",
})
}
if !isProviderConfigured {
c.JSON(422, gin.H{
"message": provider + " not configured",
})
}
}
}

View File

@@ -24,7 +24,7 @@ func VerifyEmailHandler() gin.HandlerFunc {
return
}
_, err := db.Mgr.GetVerificationByToken(token)
verificationRequest, err := db.Mgr.GetVerificationByToken(token)
if err != nil {
c.JSON(400, errorRes)
return
@@ -46,9 +46,12 @@ func VerifyEmailHandler() gin.HandlerFunc {
}
// update email_verified_at in users table
db.Mgr.UpdateVerificationTime(time.Now().Unix(), user.ID)
if user.EmailVerifiedAt <= 0 {
user.EmailVerifiedAt = time.Now().Unix()
db.Mgr.UpdateUser(user)
}
// delete from verification table
db.Mgr.DeleteToken(claim.Email)
db.Mgr.DeleteVerificationRequest(verificationRequest)
userIdStr := fmt.Sprintf("%v", user.ID)
roles := strings.Split(user.Roles, ",")
@@ -56,8 +59,17 @@ func VerifyEmailHandler() gin.HandlerFunc {
accessToken, _, _ := utils.CreateAuthToken(user, enum.AccessToken, roles)
session.SetToken(userIdStr, refreshToken)
session.SetToken(userIdStr, accessToken, refreshToken)
go func() {
sessionData := db.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(c.Request),
IP: utils.GetIP(c.Request),
}
db.Mgr.AddSession(sessionData)
}()
utils.SetCookie(c, accessToken)
c.Redirect(http.StatusTemporaryRedirect, claim.Host)
c.Redirect(http.StatusTemporaryRedirect, claim.RedirectURL)
}
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/env"
"github.com/authorizerdev/authorizer/server/handlers"
"github.com/authorizerdev/authorizer/server/oauth"
"github.com/authorizerdev/authorizer/server/session"
@@ -19,7 +20,7 @@ func GinContextToContextMiddleware() gin.HandlerFunc {
if constants.AUTHORIZER_URL == "" {
url := location.Get(c)
constants.AUTHORIZER_URL = url.Scheme + "://" + c.Request.Host
log.Println("=> setting url:", constants.AUTHORIZER_URL)
log.Println("=> authorizer url:", constants.AUTHORIZER_URL)
}
ctx := context.WithValue(c.Request.Context(), "GinContextKey", c)
c.Request = c.Request.WithContext(ctx)
@@ -32,6 +33,7 @@ func GinContextToContextMiddleware() gin.HandlerFunc {
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
constants.APP_URL = origin
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
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")
@@ -47,7 +49,7 @@ func CORSMiddleware() gin.HandlerFunc {
}
func main() {
InitEnv()
env.InitEnv()
db.InitDB()
session.InitSession()
oauth.InitOAuth()

View File

@@ -1,33 +1,49 @@
package oauth
import (
"context"
"log"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
facebookOAuth2 "golang.org/x/oauth2/facebook"
githubOAuth2 "golang.org/x/oauth2/github"
googleOAuth2 "golang.org/x/oauth2/google"
)
type OAuthProviders struct {
type OAuthProvider struct {
GoogleConfig *oauth2.Config
GithubConfig *oauth2.Config
FacebookConfig *oauth2.Config
}
var OAuthProvider OAuthProviders
type OIDCProvider struct {
GoogleOIDC *oidc.Provider
}
var (
OAuthProviders OAuthProvider
OIDCProviders OIDCProvider
)
func InitOAuth() {
ctx := context.Background()
if constants.GOOGLE_CLIENT_ID != "" && constants.GOOGLE_CLIENT_SECRET != "" {
OAuthProvider.GoogleConfig = &oauth2.Config{
p, err := oidc.NewProvider(ctx, "https://accounts.google.com")
if err != nil {
log.Fatalln("error creating oidc provider for google:", err)
}
OIDCProviders.GoogleOIDC = p
OAuthProviders.GoogleConfig = &oauth2.Config{
ClientID: constants.GOOGLE_CLIENT_ID,
ClientSecret: constants.GOOGLE_CLIENT_SECRET,
RedirectURL: constants.AUTHORIZER_URL + "/oauth_callback/google",
Endpoint: googleOAuth2.Endpoint,
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
Endpoint: OIDCProviders.GoogleOIDC.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
}
if constants.GITHUB_CLIENT_ID != "" && constants.GITHUB_CLIENT_SECRET != "" {
OAuthProvider.GithubConfig = &oauth2.Config{
OAuthProviders.GithubConfig = &oauth2.Config{
ClientID: constants.GITHUB_CLIENT_ID,
ClientSecret: constants.GITHUB_CLIENT_SECRET,
RedirectURL: constants.AUTHORIZER_URL + "/oauth_callback/github",
@@ -35,7 +51,7 @@ func InitOAuth() {
}
}
if constants.FACEBOOK_CLIENT_ID != "" && constants.FACEBOOK_CLIENT_SECRET != "" {
OAuthProvider.FacebookConfig = &oauth2.Config{
OAuthProviders.FacebookConfig = &oauth2.Config{
ClientID: constants.FACEBOOK_CLIENT_ID,
ClientSecret: constants.FACEBOOK_CLIENT_SECRET,
RedirectURL: constants.AUTHORIZER_URL + "/oauth_callback/facebook",

View File

@@ -60,7 +60,7 @@ func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*m
return res, fmt.Errorf("user with this email address already exists")
}
session.DeleteToken(fmt.Sprintf("%v", user.ID))
session.DeleteUserSession(fmt.Sprintf("%v", user.ID))
utils.DeleteCookie(gc)
user.Email = newEmail
@@ -69,7 +69,7 @@ func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*m
verificationType := enum.UpdateEmail.String()
token, err := utils.CreateVerificationToken(newEmail, verificationType)
if err != nil {
log.Println(`Error generating token`, err)
log.Println(`error generating token`, err)
}
db.Mgr.AddVerification(db.VerificationRequest{
Token: token,
@@ -100,7 +100,7 @@ func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*m
rolesToSave = strings.Join(inputRoles, ",")
}
session.DeleteToken(fmt.Sprintf("%v", user.ID))
session.DeleteUserSession(fmt.Sprintf("%v", user.ID))
utils.DeleteCookie(gc)
}
@@ -110,7 +110,7 @@ func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*m
user, err = db.Mgr.UpdateUser(user)
if err != nil {
log.Println("Error updating user:", err)
log.Println("error updating user:", err)
return res, err
}

View File

@@ -27,11 +27,11 @@ func DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Respo
return res, err
}
session.DeleteToken(fmt.Sprintf("%x", user.ID))
session.DeleteUserSession(fmt.Sprintf("%x", user.ID))
err = db.Mgr.DeleteUser(params.Email)
err = db.Mgr.DeleteUser(user)
if err != nil {
log.Println("Err:", err)
log.Println("error deleting user:", err)
return res, err
}

View File

@@ -20,7 +20,7 @@ func ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*mod
if err != nil {
return res, err
}
if constants.DISABLE_BASIC_AUTHENTICATION == "true" {
if constants.DISABLE_BASIC_AUTHENTICATION {
return res, fmt.Errorf(`basic authentication is disabled for this instance`)
}
host := gc.Request.Host
@@ -37,7 +37,7 @@ func ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*mod
token, err := utils.CreateVerificationToken(params.Email, enum.ForgotPassword.String())
if err != nil {
log.Println(`Error generating token`, err)
log.Println(`error generating token`, err)
}
db.Mgr.AddVerification(db.VerificationRequest{
Token: token,

View File

@@ -22,7 +22,7 @@ func Login(ctx context.Context, params model.LoginInput) (*model.AuthResponse, e
return res, err
}
if constants.DISABLE_BASIC_AUTHENTICATION == "true" {
if constants.DISABLE_BASIC_AUTHENTICATION {
return res, fmt.Errorf(`basic authentication is disabled for this instance`)
}
@@ -43,7 +43,7 @@ func Login(ctx context.Context, params model.LoginInput) (*model.AuthResponse, e
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(params.Password))
if err != nil {
log.Println("Compare password error:", err)
log.Println("compare password error:", err)
return res, fmt.Errorf(`invalid password`)
}
roles := constants.DEFAULT_ROLES
@@ -60,7 +60,16 @@ func Login(ctx context.Context, params model.LoginInput) (*model.AuthResponse, e
accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, roles)
session.SetToken(userIdStr, refreshToken)
session.SetToken(userIdStr, accessToken, refreshToken)
go func() {
sessionData := db.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(gc.Request),
IP: utils.GetIP(gc.Request),
}
db.Mgr.AddSession(sessionData)
}()
res = &model.AuthResponse{
Message: `Logged in successfully`,

View File

@@ -27,7 +27,7 @@ func Logout(ctx context.Context) (*model.Response, error) {
}
userId := fmt.Sprintf("%v", claim["id"])
session.DeleteToken(userId)
session.DeleteVerificationRequest(userId, token)
res = &model.Response{
Message: "Logged out successfully",
}

View File

@@ -0,0 +1,126 @@
package resolvers
import (
"context"
"fmt"
"log"
"strings"
"time"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/utils"
)
func MagicLogin(ctx context.Context, params model.MagicLoginInput) (*model.Response, error) {
var res *model.Response
if constants.DISABLE_MAGIC_LOGIN {
return res, fmt.Errorf(`magic link login is disabled for this instance`)
}
params.Email = strings.ToLower(params.Email)
if !utils.IsValidEmail(params.Email) {
return res, fmt.Errorf(`invalid email address`)
}
inputRoles := []string{}
user := db.User{
Email: params.Email,
}
// find user with email
existingUser, err := db.Mgr.GetUserByEmail(params.Email)
if err != nil {
user.SignupMethod = enum.MagicLink.String()
// define roles for new user
if len(params.Roles) > 0 {
// check if roles exists
if !utils.IsValidRoles(constants.ROLES, params.Roles) {
return res, fmt.Errorf(`invalid roles`)
} else {
inputRoles = params.Roles
}
} else {
inputRoles = constants.DEFAULT_ROLES
}
user.Roles = strings.Join(inputRoles, ",")
user, _ = db.Mgr.AddUser(user)
} else {
user = existingUser
// There multiple scenarios with roles here in magic link login
// 1. user has access to protected roles + roles and trying to login
// 2. user has not signed up for one of the available role but trying to signup.
// Need to modify roles in this case
// find the unassigned roles
existingRoles := strings.Split(existingUser.Roles, ",")
unasignedRoles := []string{}
for _, ir := range inputRoles {
if !utils.StringSliceContains(existingRoles, ir) {
unasignedRoles = append(unasignedRoles, ir)
}
}
if len(unasignedRoles) > 0 {
// check if it contains protected unassigned role
hasProtectedRole := false
for _, ur := range unasignedRoles {
if utils.StringSliceContains(constants.PROTECTED_ROLES, ur) {
hasProtectedRole = true
}
}
if hasProtectedRole {
return res, fmt.Errorf(`invalid roles`)
} else {
user.Roles = existingUser.Roles + "," + strings.Join(unasignedRoles, ",")
}
} else {
user.Roles = existingUser.Roles
}
signupMethod := existingUser.SignupMethod
if !strings.Contains(signupMethod, enum.MagicLink.String()) {
signupMethod = signupMethod + "," + enum.MagicLink.String()
}
user.SignupMethod = signupMethod
user, _ = db.Mgr.UpdateUser(user)
if err != nil {
log.Println("error updating user:", err)
}
}
if !constants.DISABLE_EMAIL_VERIFICATION {
// insert verification request
verificationType := enum.MagicLink.String()
token, err := utils.CreateVerificationToken(params.Email, verificationType)
if err != nil {
log.Println(`error generating token`, err)
}
db.Mgr.AddVerification(db.VerificationRequest{
Token: token,
Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email,
})
// exec it as go routin so that we can reduce the api latency
go func() {
utils.SendVerificationMail(params.Email, token)
}()
}
res = &model.Response{
Message: `Magic Link has been sent to your email. Please check your inbox!`,
}
return res, nil
}

View File

@@ -30,7 +30,7 @@ func Profile(ctx context.Context) (*model.User, error) {
userID := fmt.Sprintf("%v", claim["id"])
email := fmt.Sprintf("%v", claim["email"])
sessionToken := session.GetToken(userID)
sessionToken := session.GetToken(userID, token)
if sessionToken == "" {
return res, fmt.Errorf(`unauthorized`)

View File

@@ -27,7 +27,7 @@ func ResendVerifyEmail(ctx context.Context, params model.ResendVerifyEmailInput)
token, err := utils.CreateVerificationToken(params.Email, verificationRequest.Identifier)
if err != nil {
log.Println(`Error generating token`, err)
log.Println(`error generating token`, err)
}
db.Mgr.AddVerification(db.VerificationRequest{
Token: token,

View File

@@ -14,11 +14,11 @@ import (
func ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error) {
var res *model.Response
if constants.DISABLE_BASIC_AUTHENTICATION == "true" {
if constants.DISABLE_BASIC_AUTHENTICATION {
return res, fmt.Errorf(`basic authentication is disabled for this instance`)
}
_, err := db.Mgr.GetVerificationByToken(params.Token)
verificationRequest, err := db.Mgr.GetVerificationByToken(params.Token)
if err != nil {
return res, fmt.Errorf(`invalid token`)
}
@@ -48,7 +48,7 @@ func ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model
user.SignupMethod = signupMethod
// delete from verification table
db.Mgr.DeleteToken(claim.Email)
db.Mgr.DeleteVerificationRequest(verificationRequest)
db.Mgr.UpdateUser(user)
res = &model.Response{

View File

@@ -22,7 +22,7 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
return res, err
}
if constants.DISABLE_BASIC_AUTHENTICATION == "true" {
if constants.DISABLE_BASIC_AUTHENTICATION {
return res, fmt.Errorf(`basic authentication is disabled for this instance`)
}
if params.ConfirmPassword != params.Password {
@@ -51,7 +51,7 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
// find user with email
existingUser, err := db.Mgr.GetUserByEmail(params.Email)
if err != nil {
log.Println("User with email " + params.Email + " not found")
log.Println("user with email " + params.Email + " not found")
}
if existingUser.EmailVerifiedAt > 0 {
@@ -76,10 +76,10 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
}
user.SignupMethod = enum.BasicAuth.String()
if constants.DISABLE_EMAIL_VERIFICATION == "true" {
if constants.DISABLE_EMAIL_VERIFICATION {
user.EmailVerifiedAt = time.Now().Unix()
}
user, err = db.Mgr.SaveUser(user)
user, err = db.Mgr.AddUser(user)
if err != nil {
return res, err
}
@@ -98,12 +98,12 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
UpdatedAt: &user.UpdatedAt,
}
if constants.DISABLE_EMAIL_VERIFICATION != "true" {
if !constants.DISABLE_EMAIL_VERIFICATION {
// insert verification request
verificationType := enum.BasicAuthSignup.String()
token, err := utils.CreateVerificationToken(params.Email, verificationType)
if err != nil {
log.Println(`Error generating token`, err)
log.Println(`error generating token`, err)
}
db.Mgr.AddVerification(db.VerificationRequest{
Token: token,
@@ -127,7 +127,16 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, roles)
session.SetToken(userIdStr, refreshToken)
session.SetToken(userIdStr, accessToken, refreshToken)
go func() {
sessionData := db.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(gc.Request),
IP: utils.GetIP(gc.Request),
}
db.Mgr.AddSession(sessionData)
}()
res = &model.AuthResponse{
Message: `Signed up successfully.`,
AccessToken: &accessToken,

View File

@@ -37,7 +37,7 @@ func Token(ctx context.Context, roles []string) (*model.AuthResponse, error) {
userIdStr := fmt.Sprintf("%v", user.ID)
sessionToken := session.GetToken(userIdStr)
sessionToken := session.GetToken(userIdStr, token)
if sessionToken == "" {
return res, fmt.Errorf(`unauthorized`)
@@ -63,7 +63,19 @@ func Token(ctx context.Context, roles []string) (*model.AuthResponse, error) {
if accessTokenErr != nil || expiresTimeObj.Sub(currentTimeObj).Minutes() <= 5 {
// if access token has expired and refresh/session token is valid
// generate new accessToken
currentRefreshToken := session.GetToken(userIdStr, token)
session.DeleteVerificationRequest(userIdStr, token)
token, expiresAt, _ = utils.CreateAuthToken(user, enum.AccessToken, claimRoles)
session.SetToken(userIdStr, token, currentRefreshToken)
go func() {
sessionData := db.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(gc.Request),
IP: utils.GetIP(gc.Request),
}
db.Mgr.AddSession(sessionData)
}()
}
utils.SetCookie(gc, token)
@@ -80,6 +92,8 @@ func Token(ctx context.Context, roles []string) (*model.AuthResponse, error) {
Roles: strings.Split(user.Roles, ","),
CreatedAt: &user.CreatedAt,
UpdatedAt: &user.UpdatedAt,
SignupMethod: user.SignupMethod,
EmailVerifiedAt: &user.EmailVerifiedAt,
},
}
return res, nil

View File

@@ -33,7 +33,7 @@ func UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model
}
id := fmt.Sprintf("%v", claim["id"])
sessionToken := session.GetToken(id)
sessionToken := session.GetToken(id, token)
if sessionToken == "" {
return res, fmt.Errorf(`unauthorized`)
@@ -99,7 +99,7 @@ func UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model
return res, fmt.Errorf("user with this email address already exists")
}
session.DeleteToken(fmt.Sprintf("%v", user.ID))
session.DeleteUserSession(fmt.Sprintf("%v", user.ID))
utils.DeleteCookie(gc)
user.Email = newEmail
@@ -109,7 +109,7 @@ func UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model
verificationType := enum.UpdateEmail.String()
token, err := utils.CreateVerificationToken(newEmail, verificationType)
if err != nil {
log.Println(`Error generating token`, err)
log.Println(`error generating token`, err)
}
db.Mgr.AddVerification(db.VerificationRequest{
Token: token,
@@ -126,7 +126,7 @@ func UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model
_, err = db.Mgr.UpdateUser(user)
if err != nil {
log.Println("Error updating user:", err)
log.Println("error updating user:", err)
return res, err
}
message := `Profile details updated successfully.`

View File

@@ -20,7 +20,7 @@ func VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.Aut
return res, err
}
_, err = db.Mgr.GetVerificationByToken(params.Token)
verificationRequest, err := db.Mgr.GetVerificationByToken(params.Token)
if err != nil {
return res, fmt.Errorf(`invalid token`)
}
@@ -37,9 +37,10 @@ func VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.Aut
}
// update email_verified_at in users table
db.Mgr.UpdateVerificationTime(time.Now().Unix(), user.ID)
user.EmailVerifiedAt = time.Now().Unix()
db.Mgr.UpdateUser(user)
// delete from verification table
db.Mgr.DeleteToken(claim.Email)
db.Mgr.DeleteVerificationRequest(verificationRequest)
userIdStr := fmt.Sprintf("%v", user.ID)
roles := strings.Split(user.Roles, ",")
@@ -47,7 +48,16 @@ func VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.Aut
accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, roles)
session.SetToken(userIdStr, refreshToken)
session.SetToken(userIdStr, accessToken, refreshToken)
go func() {
sessionData := db.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(gc.Request),
IP: utils.GetIP(gc.Request),
}
db.Mgr.AddSession(sessionData)
}()
res = &model.AuthResponse{
Message: `Email verified successfully.`,

View File

@@ -1,41 +1,85 @@
package session
import "sync"
import (
"sync"
)
type InMemoryStore struct {
mu sync.Mutex
store map[string]string
store map[string]map[string]string
socialLoginState map[string]string
}
func (c *InMemoryStore) AddToken(userId, token string) {
func (c *InMemoryStore) AddToken(userId, accessToken, refreshToken string) {
c.mu.Lock()
// delete sessions > 500 // not recommended for production
if len(c.store) >= 500 {
c.store = make(map[string]string)
c.store = map[string]map[string]string{}
}
c.store[userId] = token
// check if entry exists in map
_, exists := c.store[userId]
if exists {
tempMap := c.store[userId]
tempMap[accessToken] = refreshToken
c.store[userId] = tempMap
} else {
tempMap := map[string]string{
accessToken: refreshToken,
}
c.store[userId] = tempMap
}
c.mu.Unlock()
}
func (c *InMemoryStore) DeleteToken(userId string) {
func (c *InMemoryStore) DeleteUserSession(userId string) {
c.mu.Lock()
delete(c.store, userId)
c.mu.Unlock()
}
func (c *InMemoryStore) ClearStore() {
func (c *InMemoryStore) DeleteVerificationRequest(userId, accessToken string) {
c.mu.Lock()
c.store = make(map[string]string)
delete(c.store[userId], accessToken)
c.mu.Unlock()
}
func (c *InMemoryStore) GetToken(userId string) string {
func (c *InMemoryStore) ClearStore() {
c.mu.Lock()
c.store = map[string]map[string]string{}
c.mu.Unlock()
}
func (c *InMemoryStore) GetToken(userId, accessToken string) string {
token := ""
c.mu.Lock()
if val, ok := c.store[userId]; ok {
if sessionMap, ok := c.store[userId]; ok {
if val, ok := sessionMap[accessToken]; ok {
token = val
}
}
c.mu.Unlock()
return token
}
func (c *InMemoryStore) SetSocialLoginState(key, state string) {
c.mu.Lock()
c.socialLoginState[key] = state
c.mu.Unlock()
}
func (c *InMemoryStore) GetSocialLoginState(key string) string {
state := ""
if stateVal, ok := c.socialLoginState[key]; ok {
state = stateVal
}
return state
}
func (c *InMemoryStore) RemoveSocialLoginState(key string) {
c.mu.Lock()
delete(c.socialLoginState, key)
c.mu.Unlock()
}

View File

@@ -2,6 +2,7 @@ package session
import (
"context"
"fmt"
"log"
"github.com/go-redis/redis/v8"
@@ -12,20 +13,29 @@ type RedisStore struct {
store *redis.Client
}
func (c *RedisStore) AddToken(userId, token string) {
err := c.store.Set(c.ctx, "authorizer_"+userId, token, 0).Err()
func (c *RedisStore) AddToken(userId, accessToken, refreshToken string) {
err := c.store.HMSet(c.ctx, "authorizer_"+userId, map[string]string{
accessToken: refreshToken,
}).Err()
if err != nil {
log.Fatalln("Error saving redis token:", err)
}
}
func (c *RedisStore) DeleteToken(userId string) {
func (c *RedisStore) DeleteUserSession(userId string) {
err := c.store.Del(c.ctx, "authorizer_"+userId).Err()
if err != nil {
log.Fatalln("Error deleting redis token:", err)
}
}
func (c *RedisStore) DeleteVerificationRequest(userId, accessToken string) {
err := c.store.HDel(c.ctx, "authorizer_"+userId, accessToken).Err()
if err != nil {
log.Fatalln("Error deleting redis token:", err)
}
}
func (c *RedisStore) ClearStore() {
err := c.store.Del(c.ctx, "authorizer_*").Err()
if err != nil {
@@ -33,11 +43,38 @@ func (c *RedisStore) ClearStore() {
}
}
func (c *RedisStore) GetToken(userId string) string {
func (c *RedisStore) GetToken(userId, accessToken string) string {
token := ""
token, err := c.store.Get(c.ctx, "authorizer_"+userId).Result()
res, err := c.store.HMGet(c.ctx, "authorizer_"+userId, accessToken).Result()
if err != nil {
log.Println("Error getting token from redis store:", err)
log.Println("error getting token from redis store:", err)
}
if len(res) > 0 && res[0] != nil {
token = fmt.Sprintf("%v", res[0])
}
return token
}
func (c *RedisStore) SetSocialLoginState(key, state string) {
err := c.store.Set(c.ctx, key, state, 0).Err()
if err != nil {
log.Fatalln("Error saving redis token:", err)
}
}
func (c *RedisStore) GetSocialLoginState(key string) string {
state := ""
state, err := c.store.Get(c.ctx, key).Result()
if err != nil {
log.Println("error getting token from redis store:", err)
}
return state
}
func (c *RedisStore) RemoveSocialLoginState(key string) {
err := c.store.Del(c.ctx, key).Err()
if err != nil {
log.Fatalln("Error deleting redis token:", err)
}
}

View File

@@ -15,30 +15,42 @@ type SessionStore struct {
var SessionStoreObj SessionStore
func SetToken(userId, token string) {
func SetToken(userId, accessToken, refreshToken string) {
// TODO: Set session information in db for all the sessions that gets generated
// it should async go function
if SessionStoreObj.RedisMemoryStoreObj != nil {
SessionStoreObj.RedisMemoryStoreObj.AddToken(userId, token)
SessionStoreObj.RedisMemoryStoreObj.AddToken(userId, accessToken, refreshToken)
}
if SessionStoreObj.InMemoryStoreObj != nil {
SessionStoreObj.InMemoryStoreObj.AddToken(userId, token)
SessionStoreObj.InMemoryStoreObj.AddToken(userId, accessToken, refreshToken)
}
}
func DeleteToken(userId string) {
func DeleteVerificationRequest(userId, accessToken string) {
if SessionStoreObj.RedisMemoryStoreObj != nil {
SessionStoreObj.RedisMemoryStoreObj.DeleteToken(userId)
SessionStoreObj.RedisMemoryStoreObj.DeleteVerificationRequest(userId, accessToken)
}
if SessionStoreObj.InMemoryStoreObj != nil {
SessionStoreObj.InMemoryStoreObj.DeleteToken(userId)
SessionStoreObj.InMemoryStoreObj.DeleteVerificationRequest(userId, accessToken)
}
}
func GetToken(userId string) string {
func DeleteUserSession(userId string) {
if SessionStoreObj.RedisMemoryStoreObj != nil {
return SessionStoreObj.RedisMemoryStoreObj.GetToken(userId)
SessionStoreObj.RedisMemoryStoreObj.DeleteUserSession(userId)
}
if SessionStoreObj.InMemoryStoreObj != nil {
return SessionStoreObj.InMemoryStoreObj.GetToken(userId)
SessionStoreObj.InMemoryStoreObj.DeleteUserSession(userId)
}
}
func GetToken(userId, accessToken string) string {
if SessionStoreObj.RedisMemoryStoreObj != nil {
return SessionStoreObj.RedisMemoryStoreObj.GetToken(userId, accessToken)
}
if SessionStoreObj.InMemoryStoreObj != nil {
return SessionStoreObj.InMemoryStoreObj.GetToken(userId, accessToken)
}
return ""
@@ -53,9 +65,38 @@ func ClearStore() {
}
}
func SetSocailLoginState(key, state string) {
if SessionStoreObj.RedisMemoryStoreObj != nil {
SessionStoreObj.RedisMemoryStoreObj.SetSocialLoginState(key, state)
}
if SessionStoreObj.InMemoryStoreObj != nil {
SessionStoreObj.InMemoryStoreObj.SetSocialLoginState(key, state)
}
}
func GetSocailLoginState(key string) string {
if SessionStoreObj.RedisMemoryStoreObj != nil {
return SessionStoreObj.RedisMemoryStoreObj.GetSocialLoginState(key)
}
if SessionStoreObj.InMemoryStoreObj != nil {
return SessionStoreObj.InMemoryStoreObj.GetSocialLoginState(key)
}
return ""
}
func RemoveSocialLoginState(key string) {
if SessionStoreObj.RedisMemoryStoreObj != nil {
SessionStoreObj.RedisMemoryStoreObj.RemoveSocialLoginState(key)
}
if SessionStoreObj.InMemoryStoreObj != nil {
SessionStoreObj.InMemoryStoreObj.RemoveSocialLoginState(key)
}
}
func InitSession() {
if constants.REDIS_URL != "" {
log.Println("Using redis store to save sessions")
log.Println("using redis store to save sessions")
opt, err := redis.ParseURL(constants.REDIS_URL)
if err != nil {
log.Fatalln("Error parsing redis url:", err)
@@ -73,9 +114,10 @@ func InitSession() {
}
} else {
log.Println("Using in memory store to save sessions")
log.Println("using in memory store to save sessions")
SessionStoreObj.InMemoryStoreObj = &InMemoryStore{
store: make(map[string]string),
store: map[string]map[string]string{},
socialLoginState: map[string]string{},
}
}
}

View File

@@ -13,7 +13,7 @@ import (
"github.com/authorizerdev/authorizer/server/enum"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
v8 "rogchap.com/v8go"
"github.com/robertkrimen/otto"
)
func CreateAuthToken(user db.User, tokenType enum.TokenType, roles []string) (string, int64, error) {
@@ -50,27 +50,26 @@ func CreateAuthToken(user db.User, tokenType enum.TokenType, roles []string) (st
"signUpMethods": strings.Split(user.SignupMethod, ","),
}
ctx, _ := v8.NewContext()
vm := otto.New()
userBytes, _ := json.Marshal(userInfo)
claimBytes, _ := json.Marshal(customClaims)
ctx.RunScript(fmt.Sprintf(`
const user = %s;
const tokenPayload = %s;
const customFunction = %s;
const functionRes = JSON.stringify(customFunction(user, tokenPayload));
`, string(userBytes), string(claimBytes), accessTokenScript), "functionCall.js")
vm.Run(fmt.Sprintf(`
var user = %s;
var tokenPayload = %s;
var customFunction = %s;
var functionRes = JSON.stringify(customFunction(user, tokenPayload));
`, string(userBytes), string(claimBytes), accessTokenScript))
val, err := ctx.RunScript("functionRes", "functionRes.js")
val, err := vm.Get("functionRes")
if err != nil {
log.Println("=> err custom access token script:", err)
log.Println("error getting custom access token script:", err)
} else {
extraPayload := make(map[string]interface{})
err = json.Unmarshal([]byte(fmt.Sprintf("%s", val)), &extraPayload)
if err != nil {
log.Println("Error converting accessTokenScript response to map:", err)
log.Println("error converting accessTokenScript response to map:", err)
} else {
for k, v := range extraPayload {
customClaims[k] = v

View File

@@ -1,7 +1,6 @@
package utils
import (
"log"
"net/http"
"github.com/authorizerdev/authorizer/server/constants"
@@ -11,18 +10,25 @@ import (
func SetCookie(gc *gin.Context, token string) {
secure := true
httpOnly := true
host := GetHostName(constants.AUTHORIZER_URL)
log.Println("=> cookie host", host)
domain := GetDomainName(constants.AUTHORIZER_URL)
if domain != "localhost" {
domain = "." + domain
}
gc.SetSameSite(http.SameSiteNoneMode)
gc.SetCookie(constants.COOKIE_NAME, token, 3600, "/", host, secure, httpOnly)
gc.SetCookie(constants.COOKIE_NAME+"-client", token, 3600, "/", domain, secure, httpOnly)
}
func GetCookie(gc *gin.Context) (string, error) {
cookie, err := gc.Request.Cookie(constants.COOKIE_NAME)
if err != nil {
cookie, err = gc.Request.Cookie(constants.COOKIE_NAME + "-client")
if err != nil {
return "", err
}
}
return cookie.Value, nil
}
@@ -31,11 +37,13 @@ func DeleteCookie(gc *gin.Context) {
secure := true
httpOnly := true
if !constants.IS_PROD {
secure = false
host := GetDomainName(constants.AUTHORIZER_URL)
domain := GetDomainName(constants.AUTHORIZER_URL)
if domain != "localhost" {
domain = "." + domain
}
host := GetHostName(constants.AUTHORIZER_URL)
gc.SetSameSite(http.SameSiteNoneMode)
gc.SetCookie(constants.COOKIE_NAME, "", -1, "/", host, secure, httpOnly)
gc.SetCookie(constants.COOKIE_NAME+"-client", "", -1, "/", domain, secure, httpOnly)
}

View File

@@ -74,7 +74,7 @@ func SendVerificationMail(toEmail, token string) error {
<tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;">
<td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;">
<p>Hey there 👋</p>
<p>We received a request to sign-up for <b>%s</b>. If this is correct, please confirm your email address by clicking the button below.</p> <br/>
<p>We received a request to sign-up / login for <b>%s</b>. If this is correct, please confirm your email address by clicking the button below.</p> <br/>
<a href="%s" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Confirm Email</a>
</td>
</tr>

View File

@@ -1,30 +1,6 @@
package utils
import (
"log"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
)
// any jobs that we want to run at start of server can be executed here
// 1. create roles table and add the roles list from env to table
func InitServer() {
roles := []db.Role{}
for _, val := range constants.ROLES {
roles = append(roles, db.Role{
Role: val,
})
}
for _, val := range constants.PROTECTED_ROLES {
roles = append(roles, db.Role{
Role: val,
})
}
err := db.Mgr.SaveRoles(roles)
if err != nil {
log.Println(`Error saving roles`, err)
}
}

View File

@@ -14,7 +14,8 @@ func GetMetaInfo() model.Meta {
IsGithubLoginEnabled: constants.GITHUB_CLIENT_ID != "" && constants.GOOGLE_CLIENT_SECRET != "",
IsFacebookLoginEnabled: constants.FACEBOOK_CLIENT_ID != "" && constants.FACEBOOK_CLIENT_SECRET != "",
IsTwitterLoginEnabled: constants.TWITTER_CLIENT_ID != "" && constants.TWITTER_CLIENT_SECRET != "",
IsBasicAuthenticationEnabled: constants.DISABLE_BASIC_AUTHENTICATION != "true",
IsEmailVerificationEnabled: constants.DISABLE_EMAIL_VERIFICATION != "true",
IsBasicAuthenticationEnabled: !constants.DISABLE_BASIC_AUTHENTICATION,
IsEmailVerificationEnabled: !constants.DISABLE_EMAIL_VERIFICATION,
IsMagicLoginEnabled: !constants.DISABLE_MAGIC_LOGIN,
}
}

View File

@@ -0,0 +1,19 @@
package utils
import "net/http"
func GetIP(r *http.Request) string {
IPAddress := r.Header.Get("X-Real-Ip")
if IPAddress == "" {
IPAddress = r.Header.Get("X-Forwarded-For")
}
if IPAddress == "" {
IPAddress = r.RemoteAddr
}
return IPAddress
}
func GetUserAgent(r *http.Request) string {
return r.UserAgent()
}

View File

@@ -5,7 +5,7 @@ import (
"strings"
)
// function to get hostname
// GetHostName function to get hostname
func GetHostName(auth_url string) string {
u, err := url.Parse(auth_url)
if err != nil {
@@ -17,7 +17,7 @@ func GetHostName(auth_url string) string {
return host
}
// function to get domain name
// GetDomainName function to get domain name
func GetDomainName(auth_url string) string {
u, err := url.Parse(auth_url)
if err != nil {

25
server/utils/urls_test.go Normal file
View File

@@ -0,0 +1,25 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetHostName(t *testing.T) {
authorizer_url := "http://test.herokuapp.com"
got := GetHostName(authorizer_url)
want := "test.herokuapp.com"
assert.Equal(t, got, want, "hostname should be equal")
}
func TestGetDomainName(t *testing.T) {
authorizer_url := "http://test.herokuapp.com"
got := GetDomainName(authorizer_url)
want := "herokuapp.com"
assert.Equal(t, got, want, "domain name should be equal")
}

View File

@@ -0,0 +1,17 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsValidEmail(t *testing.T) {
validEmail := "lakhan@gmail.com"
invalidEmail1 := "lakhan"
invalidEmail2 := "lakhan.me"
assert.True(t, IsValidEmail(validEmail), "it should be valid email")
assert.False(t, IsValidEmail(invalidEmail1), "it should be invalid email")
assert.False(t, IsValidEmail(invalidEmail2), "it should be invalid email")
}

View File

@@ -10,6 +10,7 @@ import (
type UserInfo struct {
Email string `json:"email"`
Host string `json:"host"`
RedirectURL string `json:"redirect_url"`
}
type CustomClaim struct {
@@ -18,7 +19,6 @@ type CustomClaim struct {
UserInfo
}
// TODO convert tokenType to enum
func CreateVerificationToken(email string, tokenType string) (string, error) {
t := jwt.New(jwt.GetSigningMethod(constants.JWT_TYPE))
@@ -28,7 +28,7 @@ func CreateVerificationToken(email string, tokenType string) (string, error) {
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
},
tokenType,
UserInfo{Email: email, Host: constants.AUTHORIZER_URL},
UserInfo{Email: email, Host: constants.AUTHORIZER_URL, RedirectURL: constants.APP_URL},
}
return t.SignedString([]byte(constants.JWT_SECRET))