Compare commits

...

23 Commits

Author SHA1 Message Date
Lakhan Samani
8f7582e1ec fix: add valid origin check for cors (#83)
Resolves #72
2021-12-21 18:46:54 +05:30
Lakhan Samani
bdbbe4adee Update README.md 2021-12-21 09:32:05 +05:30
Lakhan Samani
65478296cb feat: add mongodb support (#82)
* feat: add mongodb enum

* fix: isMongodb var condition

* feat: add init mongodb connection

* feat: add mongodb operations for various db methods

* fix: error message
2021-12-20 23:21:27 +05:30
Lakhan Samani
2342f7c5c6 build: new app 2021-12-20 18:42:02 +05:30
Lakhan Samani
8266c1cff5 chore: update authorizer-react 0.2.0 2021-12-20 18:41:48 +05:30
Lakhan Samani
c662c625a0 Update README.md 2021-12-20 18:38:17 +05:30
Lakhan Samani
c989648327 Merge branch 'main' of https://github.com/authorizerdev/authorizer 2021-12-20 18:32:46 +05:30
Lakhan Samani
a933ac1118 fix: remove count check on cursor 2021-12-20 18:32:36 +05:30
Lakhan Samani
b8afe7abcc Update README.md 2021-12-20 18:28:55 +05:30
Lakhan Samani
3ab02cc4ff Update README.md 2021-12-20 18:22:56 +05:30
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
55 changed files with 1533 additions and 1856 deletions

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? ## Where to ask questions?
1. Check our [Github Issues](https://github.com/authorizerdev/authorizer/issues) to see if someone has already answered your question. 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: 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) 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 🙂. 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 ### 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 WORKDIR /app
COPY server server COPY server server
COPY Makefile . COPY Makefile .

View File

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

View File

@@ -7,7 +7,7 @@
Authorizer Authorizer
</h1> </h1>
**Authorizer** is an open-source authentication and authorization solution for your applications. Bring your database and have complete control over the user information. You can self-host authorizer instances and connect to any SQL database. **Authorizer** is an open-source authentication and authorization solution for your applications. Bring your database and have complete control over the user information. You can self-host authorizer instances and connect to any database (Currently supports [Postgres](https://www.postgresql.org/), [MySQL](https://www.mysql.com/), [SQLite](https://www.sqlite.org/index.html), [SQLServer](https://www.microsoft.com/en-us/sql-server/), [MongoDB](https://mongodb.com/),[ArangoDB](https://www.arangodb.com/)).
## Table of contents ## Table of contents
@@ -15,7 +15,7 @@
- [Getting Started](#getting-started) - [Getting Started](#getting-started)
- [Contributing](https://github.com/authorizerdev/authorizer/blob/main/.github/CONTRIBUTING.md) - [Contributing](https://github.com/authorizerdev/authorizer/blob/main/.github/CONTRIBUTING.md)
- [Docs](http://docs.authorizer.dev/) - [Docs](http://docs.authorizer.dev/)
- [Join Community](https://discord.gg/2fXUQN3E) - [Join Community](https://discord.gg/Zv2D5h6kkK)
# Introduction # Introduction
@@ -32,10 +32,6 @@
- ✅ Role-based access management - ✅ Role-based access management
- ✅ Password-less login with email and magic link - ✅ Password-less login with email and magic link
## 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!
## Roadmap ## Roadmap
- Support more JWT encryption algorithms (Currently supporting HS256) - Support more JWT encryption algorithms (Currently supporting HS256)
@@ -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 source code](#install-using-source-code)
- [Install using binaries](#install-using-binaries) - [Install using binaries](#install-using-binaries)
- [Install instance on heroku](#install-instance-on-Heroku) - [Install instance on heroku](#install-instance-on-Heroku)
- [Install instance on railway.app](#install-instance-on-railway)
## Install using source code ## Install using source code
@@ -138,12 +135,23 @@ Deploy Authorizer using [heroku](https://github.com/authorizerdev/authorizer-her
<br/><br/> <br/><br/>
[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku) [![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 ### Things to consider
- For social logins, you will need respective social platform key and secret - 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. - 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 😅 > 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.
## Testing
- Integration Test: This tests are written along with [authorizer-js](https://github.com/authorizerdev/authorizer-js/blob/main/__test__/index.test.js) lib.
## Integrating into your website ## Integrating into your website
@@ -184,3 +192,9 @@ This example demonstrates how you can use [`@authorizerdev/authorizer-js`](/auth
onLoad(); onLoad();
</script> </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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1006
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package constants
var ( var (
// Ref: https://github.com/qor/auth/blob/master/providers/google/google.go // 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" GoogleUserInfoURL = "https://www.googleapis.com/oauth2/v3/userinfo"
// Ref: https://github.com/qor/auth/blob/master/providers/facebook/facebook.go#L18 // 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=" FacebookUserInfoURL = "https://graph.facebook.com/me?fields=id,first_name,last_name,name,email,picture&access_token="

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

@@ -0,0 +1,112 @@
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
}
arangoClient, err := arangoDriver.NewClient(arangoDriver.ClientConfig{
Connection: conn,
})
if err != nil {
return nil, err
}
var arangodb driver.Database
arangodb_exists, err := arangoClient.DatabaseExists(nil, constants.DATABASE_NAME)
if arangodb_exists {
log.Println(constants.DATABASE_NAME + " db exists already")
arangodb, err = arangoClient.Database(nil, constants.DATABASE_NAME)
if err != nil {
return nil, err
}
} else {
arangodb, err = arangoClient.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{
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,62 +3,126 @@ package db
import ( import (
"log" "log"
arangoDriver "github.com/arangodb/go-driver"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/enum" "github.com/authorizerdev/authorizer/server/enum"
"github.com/google/uuid" "go.mongodb.org/mongo-driver/mongo"
"gorm.io/driver/mysql" "gorm.io/driver/mysql"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/driver/sqlserver"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/schema" "gorm.io/gorm/schema"
) )
type Manager interface { type Manager interface {
SaveUser(user User) (User, error) AddUser(user User) (User, error)
UpdateUser(user User) (User, error) UpdateUser(user User) (User, error)
DeleteUser(user User) error
GetUsers() ([]User, error) GetUsers() ([]User, error)
GetUserByEmail(email string) (User, error) GetUserByEmail(email string) (User, error)
GetUserByID(email string) (User, error) GetUserByID(email string) (User, error)
UpdateVerificationTime(verifiedAt int64, id uuid.UUID) error
AddVerification(verification VerificationRequest) (VerificationRequest, error) AddVerification(verification VerificationRequest) (VerificationRequest, error)
GetVerificationByToken(token string) (VerificationRequest, error) GetVerificationByToken(token string) (VerificationRequest, error)
DeleteToken(email string) error DeleteVerificationRequest(verificationRequest VerificationRequest) error
GetVerificationRequests() ([]VerificationRequest, error) GetVerificationRequests() ([]VerificationRequest, error)
GetVerificationByEmail(email string) (VerificationRequest, error) GetVerificationByEmail(email string) (VerificationRequest, error)
DeleteUser(email string) error AddSession(session Session) error
SaveRoles(roles []Role) error
SaveSession(session Session) error
} }
type manager struct { type manager struct {
db *gorm.DB sqlDB *gorm.DB
arangodb arangoDriver.Database
mongodb *mongo.Database
} }
var Mgr Manager // mainly used by nosql dbs
type CollectionList struct {
User string
VerificationRequest string
Session string
}
var (
IsORMSupported bool
IsArangoDB bool
IsMongoDB bool
Mgr Manager
Prefix = "authorizer_"
Collections = CollectionList{
User: Prefix + "users",
VerificationRequest: Prefix + "verification_requests",
Session: Prefix + "sessions",
}
)
func InitDB() { func InitDB() {
var db *gorm.DB var sqlDB *gorm.DB
var err error var err error
IsORMSupported = constants.DATABASE_TYPE != enum.Arangodb.String() && constants.DATABASE_TYPE != enum.Mongodb.String()
IsArangoDB = constants.DATABASE_TYPE == enum.Arangodb.String()
IsMongoDB = constants.DATABASE_TYPE == enum.Mongodb.String()
// sql db orm config
ormConfig := &gorm.Config{ ormConfig := &gorm.Config{
NamingStrategy: schema.NamingStrategy{ NamingStrategy: schema.NamingStrategy{
TablePrefix: "authorizer_", TablePrefix: Prefix,
}, },
} }
if constants.DATABASE_TYPE == enum.Postgres.String() {
db, err = gorm.Open(postgres.Open(constants.DATABASE_URL), ormConfig) log.Println("db type:", constants.DATABASE_TYPE)
}
if constants.DATABASE_TYPE == enum.Mysql.String() { switch constants.DATABASE_TYPE {
db, err = gorm.Open(mysql.Open(constants.DATABASE_URL), ormConfig) case enum.Postgres.String():
} sqlDB, err = gorm.Open(postgres.Open(constants.DATABASE_URL), ormConfig)
if constants.DATABASE_TYPE == enum.Sqlite.String() { break
db, err = gorm.Open(sqlite.Open(constants.DATABASE_URL), ormConfig) 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("error initializing arangodb:", err)
}
Mgr = &manager{
sqlDB: nil,
mongodb: nil,
arangodb: arangodb,
}
break
case enum.Mongodb.String():
mongodb, err := initMongodb()
if err != nil {
log.Fatal("error initializing mongodb connection:", err)
}
Mgr = &manager{
sqlDB: nil,
arangodb: nil,
mongodb: mongodb,
}
} }
if err != nil { // common for all sql dbs that are configured via go-orm
log.Fatal("Failed to init db:", err) if IsORMSupported {
} else { if err != nil {
db.AutoMigrate(&User{}, &VerificationRequest{}, &Role{}, &Session{}) log.Fatal("Failed to init sqlDB:", err)
} else {
sqlDB.AutoMigrate(&User{}, &VerificationRequest{}, &Session{})
}
Mgr = &manager{
sqlDB: sqlDB,
arangodb: nil,
mongodb: nil,
}
} }
Mgr = &manager{db: db}
} }

81
server/db/mongodb.go Normal file
View File

@@ -0,0 +1,81 @@
package db
import (
"context"
"time"
"github.com/authorizerdev/authorizer/server/constants"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
func initMongodb() (*mongo.Database, error) {
mongodbOptions := options.Client().ApplyURI(constants.DATABASE_URL)
maxWait := time.Duration(5 * time.Second)
mongodbOptions.ConnectTimeout = &maxWait
mongoClient, err := mongo.NewClient(mongodbOptions)
if err != nil {
return nil, err
}
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
err = mongoClient.Connect(ctx)
if err != nil {
return nil, err
}
err = mongoClient.Ping(ctx, readpref.Primary())
if err != nil {
return nil, err
}
mongodb := mongoClient.Database(constants.DATABASE_NAME, options.Database())
mongodb.CreateCollection(ctx, Collections.User, options.CreateCollection())
userCollection := mongodb.Collection(Collections.User, options.Collection())
userCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
mongo.IndexModel{
Keys: bson.M{"id": 1},
Options: options.Index().SetUnique(true).SetSparse(true),
},
}, options.CreateIndexes())
userCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
mongo.IndexModel{
Keys: bson.M{"email": 1},
Options: options.Index().SetUnique(true).SetSparse(true),
},
}, options.CreateIndexes())
mongodb.CreateCollection(ctx, Collections.VerificationRequest, options.CreateCollection())
verificationRequestCollection := mongodb.Collection(Collections.VerificationRequest, options.Collection())
verificationRequestCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
mongo.IndexModel{
Keys: bson.M{"id": 1},
Options: options.Index().SetUnique(true).SetSparse(true),
},
}, options.CreateIndexes())
verificationRequestCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
mongo.IndexModel{
Keys: bson.M{"email": 1, "identifier": 1},
Options: options.Index().SetUnique(true).SetSparse(true),
},
}, options.CreateIndexes())
verificationRequestCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
mongo.IndexModel{
Keys: bson.M{"token": 1},
Options: options.Index().SetSparse(true),
},
}, options.CreateIndexes())
mongodb.CreateCollection(ctx, Collections.Session, options.CreateCollection())
sessionCollection := mongodb.Collection(Collections.Session, options.Collection())
sessionCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
mongo.IndexModel{
Keys: bson.M{"id": 1},
Options: options.Index().SetUnique(true).SetSparse(true),
},
}, options.CreateIndexes())
return mongodb, 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
}

View File

@@ -2,37 +2,66 @@ package db
import ( import (
"log" "log"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "go.mongodb.org/mongo-driver/mongo/options"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
type Session struct { type Session struct {
ID uuid.UUID `gorm:"type:uuid;"` Key string `json:"_key,omitempty" bson:"_key,omitempty"` // for arangodb
UserID uuid.UUID `gorm:"type:uuid;"` ObjectID string `json:"_id,omitempty" bson:"_id"` // for arangodb & mongodb
User User ID string `gorm:"primaryKey;type:char(36)" json:"id" bson:"id"`
UserAgent string UserID string `gorm:"type:char(36)" json:"user_id" bson:"user_id"`
IP string User User `json:"-" bson:"-"`
CreatedAt int64 `gorm:"autoCreateTime"` UserAgent string `json:"user_agent" bson:"user_agent"`
UpdatedAt int64 `gorm:"autoUpdateTime"` IP string `json:"ip" bson:"ip"`
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
} }
func (r *Session) BeforeCreate(tx *gorm.DB) (err error) { // AddSession function to save user sessiosn
r.ID = uuid.New() func (mgr *manager) AddSession(session Session) error {
if session.ID == "" {
session.ID = uuid.New().String()
}
return if IsORMSupported {
} 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
}
}
// SaveSession function to save user sessiosn if IsArangoDB {
func (mgr *manager) SaveSession(session Session) error { session.CreatedAt = time.Now().Unix()
res := mgr.db.Clauses( session.UpdatedAt = time.Now().Unix()
clause.OnConflict{ sessionCollection, _ := mgr.arangodb.Collection(nil, Collections.Session)
DoNothing: true, _, err := sessionCollection.CreateDocument(nil, session)
}).Create(&session) if err != nil {
if res.Error != nil { log.Println(`error saving session`, err)
log.Println(`Error saving session`, res.Error) return err
return res.Error }
}
if IsMongoDB {
session.Key = session.ID
session.ObjectID = session.ID
session.CreatedAt = time.Now().Unix()
session.UpdatedAt = time.Now().Unix()
sessionCollection := mgr.mongodb.Collection(Collections.Session, options.Collection())
_, err := sessionCollection.InsertOne(nil, session)
if err != nil {
log.Println(`error saving session`, err)
return err
}
} }
return nil return nil

View File

@@ -1,82 +1,224 @@
package db package db
import ( import (
"fmt"
"log" "log"
"time" "time"
"github.com/arangodb/go-driver"
arangoDriver "github.com/arangodb/go-driver"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
type User struct { type User struct {
ID uuid.UUID `gorm:"type:uuid;"` Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
FirstName string ObjectID string `json:"_id,omitempty" bson:"_id"` // for arangodb & mongodb
LastName string ID string `gorm:"primaryKey;type:char(36)" json:"id" bson:"id"`
Email string `gorm:"unique"` FirstName string `json:"first_name" bson:"first_name"`
Password string LastName string `json:"last_name" bson:"last_name"`
SignupMethod string Email string `gorm:"unique" json:"email" bson:"email"`
EmailVerifiedAt int64 Password string `gorm:"type:text" json:"password" bson:"password"`
CreatedAt int64 `gorm:"autoCreateTime"` SignupMethod string `json:"signup_method" bson:"signup_method"`
UpdatedAt int64 `gorm:"autoUpdateTime"` EmailVerifiedAt int64 `json:"email_verified_at" bson:"email_verified_at"`
Image string CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
Roles string UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
Image string `gorm:"type:text" json:"image" bson:"image"`
Roles string `json:"roles" bson:"roles"`
} }
func (u *User) BeforeCreate(tx *gorm.DB) (err error) { // AddUser function to add user even with email conflict
u.ID = uuid.New() func (mgr *manager) AddUser(user User) (User, error) {
if user.ID == "" {
return user.ID = uuid.New().String()
}
// SaveUser function to add user even with email conflict
func (mgr *manager) SaveUser(user User) (User, error) {
result := mgr.db.Clauses(
clause.OnConflict{
UpdateAll: true,
Columns: []clause.Column{{Name: "email"}},
}).Create(&user)
if result.Error != nil {
log.Println(result.Error)
return user, result.Error
} }
if IsORMSupported {
// 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("error adding user:", result.Error)
return user, result.Error
}
}
if IsArangoDB {
user.CreatedAt = time.Now().Unix()
user.UpdatedAt = time.Now().Unix()
userCollection, _ := mgr.arangodb.Collection(nil, Collections.User)
meta, err := userCollection.CreateDocument(arangoDriver.WithOverwrite(nil), user)
if err != nil {
log.Println("error adding user:", err)
return user, err
}
user.Key = meta.Key
user.ObjectID = meta.ID.String()
}
if IsMongoDB {
user.CreatedAt = time.Now().Unix()
user.UpdatedAt = time.Now().Unix()
user.Key = user.ID
user.ObjectID = user.ID
userCollection := mgr.mongodb.Collection(Collections.User, options.Collection())
_, err := userCollection.InsertOne(nil, user)
if err != nil {
log.Println("error adding user:", err)
return user, err
}
}
return user, nil return user, nil
} }
// UpdateUser function to update user with ID conflict // UpdateUser function to update user with ID conflict
func (mgr *manager) UpdateUser(user User) (User, error) { func (mgr *manager) UpdateUser(user User) (User, error) {
user.UpdatedAt = time.Now().Unix() user.UpdatedAt = time.Now().Unix()
result := mgr.db.Clauses(
clause.OnConflict{
UpdateAll: true,
Columns: []clause.Column{{Name: "email"}},
}).Create(&user)
if result.Error != nil { if IsORMSupported {
log.Println(result.Error) result := mgr.sqlDB.Save(&user)
return user, result.Error
if result.Error != nil {
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()
}
if IsMongoDB {
userCollection := mgr.mongodb.Collection(Collections.User, options.Collection())
_, err := userCollection.UpdateOne(nil, bson.M{"id": bson.M{"$eq": user.ID}}, bson.M{"$set": user}, options.MergeUpdateOptions())
if err != nil {
log.Println("error updating user:", err)
return user, err
}
}
return user, nil return user, nil
} }
// GetUsers function to get all users // GetUsers function to get all users
func (mgr *manager) GetUsers() ([]User, error) { func (mgr *manager) GetUsers() ([]User, error) {
var users []User var users []User
result := mgr.db.Find(&users)
if result.Error != nil { if IsORMSupported {
log.Println(result.Error) result := mgr.sqlDB.Find(&users)
return users, result.Error if result.Error != nil {
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 != "" {
users = append(users, user)
}
}
}
if IsMongoDB {
userCollection := mgr.mongodb.Collection(Collections.User, options.Collection())
cursor, err := userCollection.Find(nil, bson.M{}, options.Find())
if err != nil {
log.Println("error getting users:", err)
return users, err
}
defer cursor.Close(nil)
for cursor.Next(nil) {
var user User
err := cursor.Decode(&user)
if err != nil {
return users, err
}
users = append(users, user)
}
}
return users, nil return users, nil
} }
func (mgr *manager) GetUserByEmail(email string) (User, error) { func (mgr *manager) GetUserByEmail(email string) (User, error) {
var user User var user User
result := mgr.db.Where("email = ?", email).First(&user)
if result.Error != nil { if IsORMSupported {
return user, result.Error 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
}
}
}
if IsMongoDB {
userCollection := mgr.mongodb.Collection(Collections.User, options.Collection())
err := userCollection.FindOne(nil, bson.M{"email": email}).Decode(&user)
if err != nil {
return user, err
}
} }
return user, nil return user, nil
@@ -84,35 +226,78 @@ func (mgr *manager) GetUserByEmail(email string) (User, error) {
func (mgr *manager) GetUserByID(id string) (User, error) { func (mgr *manager) GetUserByID(id string) (User, error) {
var user User var user User
result := mgr.db.Where("id = ?", id).First(&user)
if result.Error != nil { if IsORMSupported {
return user, result.Error 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()
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
}
}
}
if IsMongoDB {
userCollection := mgr.mongodb.Collection(Collections.User, options.Collection())
err := userCollection.FindOne(nil, bson.M{"id": id}).Decode(&user)
if err != nil {
return user, err
}
} }
return user, nil return user, nil
} }
func (mgr *manager) UpdateVerificationTime(verifiedAt int64, id uuid.UUID) error { func (mgr *manager) DeleteUser(user User) error {
user := &User{ if IsORMSupported {
ID: id, result := mgr.sqlDB.Delete(&user)
}
result := mgr.db.Model(&user).Where("id = ?", id).Update("email_verified_at", verifiedAt)
if result.Error != nil { if result.Error != nil {
return result.Error log.Println(`error deleting user:`, result.Error)
} return result.Error
}
return nil }
}
if IsArangoDB {
func (mgr *manager) DeleteUser(email string) error { collection, _ := mgr.arangodb.Collection(nil, Collections.User)
var user User _, err := collection.RemoveDocument(nil, user.Key)
result := mgr.db.Where("email = ?", email).Delete(&user) if err != nil {
log.Println(`error deleting user:`, err)
if result.Error != nil { return err
log.Println(`Error deleting user:`, result.Error) }
return result.Error }
if IsMongoDB {
userCollection := mgr.mongodb.Collection(Collections.User, options.Collection())
_, err := userCollection.DeleteOne(nil, bson.M{"id": user.ID}, options.Delete())
if err != nil {
log.Println("error deleting user:", err)
return err
}
} }
return nil return nil

View File

@@ -1,50 +1,182 @@
package db package db
import ( import (
"fmt"
"log" "log"
"time"
"github.com/arangodb/go-driver"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
type VerificationRequest struct { type VerificationRequest struct {
ID uuid.UUID `gorm:"type:uuid;"` Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
Token string `gorm:"index"` ObjectID string `json:"_id,omitempty" bson:"_id"` // for arangodb & mongodb
Identifier string ID string `gorm:"primaryKey;type:char(36)" json:"id" bson:"id"`
ExpiresAt int64 Token string `gorm:"type:text" json:"token" bson:"token"`
CreatedAt int64 `gorm:"autoCreateTime"` Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier" bson:"identifier"`
UpdatedAt int64 `gorm:"autoUpdateTime"` ExpiresAt int64 `json:"expires_at" bson:"expires_at"`
Email string `gorm:"unique"` CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
} UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"`
func (v *VerificationRequest) BeforeCreate(tx *gorm.DB) (err error) {
v.ID = uuid.New()
return
} }
// AddVerification function to add verification record // AddVerification function to add verification record
func (mgr *manager) AddVerification(verification VerificationRequest) (VerificationRequest, error) { func (mgr *manager) AddVerification(verification VerificationRequest) (VerificationRequest, error) {
result := mgr.db.Clauses(clause.OnConflict{ if verification.ID == "" {
Columns: []clause.Column{{Name: "email"}}, verification.ID = uuid.New().String()
DoUpdates: clause.AssignmentColumns([]string{"token", "identifier", "expires_at"}),
}).Create(&verification)
if result.Error != nil {
log.Println(`Error saving verification record`, result.Error)
return verification, result.Error
} }
if IsORMSupported {
// 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)
return verification, result.Error
}
}
if IsArangoDB {
verification.CreatedAt = time.Now().Unix()
verification.UpdatedAt = time.Now().Unix()
verificationRequestCollection, _ := mgr.arangodb.Collection(nil, Collections.VerificationRequest)
meta, err := verificationRequestCollection.CreateDocument(nil, verification)
if err != nil {
log.Println("error saving verification record:", err)
return verification, err
}
verification.Key = meta.Key
verification.ObjectID = meta.ID.String()
}
if IsMongoDB {
verification.CreatedAt = time.Now().Unix()
verification.UpdatedAt = time.Now().Unix()
verification.Key = verification.ID
verification.ObjectID = verification.ID
verificationRequestCollection := mgr.mongodb.Collection(Collections.VerificationRequest, options.Collection())
_, err := verificationRequestCollection.InsertOne(nil, verification)
if err != nil {
log.Println("error saving verification record:", err)
return verification, err
}
}
return verification, nil return verification, nil
} }
// GetVerificationRequests function to get all verification requests
func (mgr *manager) GetVerificationRequests() ([]VerificationRequest, error) {
var verificationRequests []VerificationRequest
if IsORMSupported {
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 != "" {
verificationRequests = append(verificationRequests, verificationRequest)
}
}
}
if IsMongoDB {
verificationRequestCollection := mgr.mongodb.Collection(Collections.VerificationRequest, options.Collection())
cursor, err := verificationRequestCollection.Find(nil, bson.M{}, options.Find())
if err != nil {
log.Println("error getting verification requests:", err)
return verificationRequests, err
}
defer cursor.Close(nil)
for cursor.Next(nil) {
var verificationRequest VerificationRequest
err := cursor.Decode(&verificationRequest)
if err != nil {
return verificationRequests, err
}
verificationRequests = append(verificationRequests, verificationRequest)
}
}
return verificationRequests, nil
}
func (mgr *manager) GetVerificationByToken(token string) (VerificationRequest, error) { func (mgr *manager) GetVerificationByToken(token string) (VerificationRequest, error) {
var verification VerificationRequest var verification VerificationRequest
result := mgr.db.Where("token = ?", token).First(&verification)
if result.Error != nil { if IsORMSupported {
log.Println(`Error getting verification token:`, result.Error) result := mgr.sqlDB.Where("token = ?", token).First(&verification)
return verification, result.Error
if result.Error != nil {
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
}
}
}
if IsMongoDB {
verificationRequestCollection := mgr.mongodb.Collection(Collections.VerificationRequest, options.Collection())
err := verificationRequestCollection.FindOne(nil, bson.M{"token": token}).Decode(&verification)
if err != nil {
return verification, err
}
} }
return verification, nil return verification, nil
@@ -52,35 +184,79 @@ func (mgr *manager) GetVerificationByToken(token string) (VerificationRequest, e
func (mgr *manager) GetVerificationByEmail(email string) (VerificationRequest, error) { func (mgr *manager) GetVerificationByEmail(email string) (VerificationRequest, error) {
var verification VerificationRequest var verification VerificationRequest
result := mgr.db.Where("email = ?", email).First(&verification) if IsORMSupported {
result := mgr.sqlDB.Where("email = ?", email).First(&verification)
if result.Error != nil { if result.Error != nil {
log.Println(`Error getting verification token:`, result.Error) log.Println(`error getting verification token:`, result.Error)
return verification, 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
}
}
}
if IsMongoDB {
verificationRequestCollection := mgr.mongodb.Collection(Collections.VerificationRequest, options.Collection())
err := verificationRequestCollection.FindOne(nil, bson.M{"email": email}).Decode(&verification)
if err != nil {
return verification, err
}
} }
return verification, nil return verification, nil
} }
func (mgr *manager) DeleteToken(email string) error { func (mgr *manager) DeleteVerificationRequest(verificationRequest VerificationRequest) error {
var verification VerificationRequest if IsORMSupported {
result := mgr.db.Where("email = ?", email).Delete(&verification) result := mgr.sqlDB.Delete(&verificationRequest)
if result.Error != nil { if result.Error != nil {
log.Println(`Error deleting token:`, result.Error) log.Println(`error deleting verification request:`, result.Error)
return 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
}
}
if IsMongoDB {
verificationRequestCollection := mgr.mongodb.Collection(Collections.VerificationRequest, options.Collection())
_, err := verificationRequestCollection.DeleteOne(nil, bson.M{"id": verificationRequest.ID}, options.Delete())
if err != nil {
log.Println("error deleting verification request::", err)
return err
}
} }
return nil 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,9 @@ const (
Postgres DbType = iota Postgres DbType = iota
Sqlite Sqlite
Mysql Mysql
SQLServer
Arangodb
Mongodb
) )
func (d DbType) String() string { func (d DbType) String() string {
@@ -13,5 +16,8 @@ func (d DbType) String() string {
"postgres", "postgres",
"sqlite", "sqlite",
"mysql", "mysql",
"sqlserver",
"arangodb",
"mongodb",
}[d] }[d]
} }

View File

@@ -1,4 +1,4 @@
package main package env
import ( import (
"flag" "flag"
@@ -13,7 +13,7 @@ import (
// build variables // build variables
var ( var (
Version string VERSION string
ARG_DB_URL *string ARG_DB_URL *string
ARG_DB_TYPE *string ARG_DB_TYPE *string
ARG_AUTHORIZER_URL *string ARG_AUTHORIZER_URL *string
@@ -22,7 +22,9 @@ var (
// InitEnv -> to initialize env and through error if required env are not present // InitEnv -> to initialize env and through error if required env are not present
func InitEnv() { func InitEnv() {
envPath := `.env` if constants.ENV_PATH == "" {
constants.ENV_PATH = `.env`
}
ARG_DB_URL = flag.String("database_url", "", "Database connection string") 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_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") ARG_AUTHORIZER_URL = flag.String("authorizer_url", "", "URL for authorizer instance, eg: https://xyz.herokuapp.com")
@@ -30,19 +32,20 @@ func InitEnv() {
flag.Parse() flag.Parse()
if *ARG_ENV_FILE != "" { 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 { 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.ADMIN_SECRET = os.Getenv("ADMIN_SECRET")
constants.ENV = os.Getenv("ENV") constants.ENV = os.Getenv("ENV")
constants.DATABASE_TYPE = os.Getenv("DATABASE_TYPE") constants.DATABASE_TYPE = os.Getenv("DATABASE_TYPE")
constants.DATABASE_URL = os.Getenv("DATABASE_URL") constants.DATABASE_URL = os.Getenv("DATABASE_URL")
constants.DATABASE_NAME = os.Getenv("DATABASE_NAME")
constants.SMTP_HOST = os.Getenv("SMTP_HOST") constants.SMTP_HOST = os.Getenv("SMTP_HOST")
constants.SMTP_PORT = os.Getenv("SMTP_PORT") constants.SMTP_PORT = os.Getenv("SMTP_PORT")
constants.SENDER_EMAIL = os.Getenv("SENDER_EMAIL") constants.SENDER_EMAIL = os.Getenv("SENDER_EMAIL")
@@ -62,9 +65,9 @@ func InitEnv() {
constants.TWITTER_CLIENT_ID = os.Getenv("TWITTER_CLIENT_ID") constants.TWITTER_CLIENT_ID = os.Getenv("TWITTER_CLIENT_ID")
constants.TWITTER_CLIENT_SECRET = os.Getenv("TWITTER_CLIENT_SECRET") constants.TWITTER_CLIENT_SECRET = os.Getenv("TWITTER_CLIENT_SECRET")
constants.RESET_PASSWORD_URL = strings.TrimPrefix(os.Getenv("RESET_PASSWORD_URL"), "/") constants.RESET_PASSWORD_URL = strings.TrimPrefix(os.Getenv("RESET_PASSWORD_URL"), "/")
constants.DISABLE_BASIC_AUTHENTICATION = os.Getenv("DISABLE_BASIC_AUTHENTICATION") constants.DISABLE_BASIC_AUTHENTICATION = os.Getenv("DISABLE_BASIC_AUTHENTICATION") == "true"
constants.DISABLE_EMAIL_VERIFICATION = os.Getenv("DISABLE_EMAIL_VERIFICATION") constants.DISABLE_EMAIL_VERIFICATION = os.Getenv("DISABLE_EMAIL_VERIFICATION") == "true"
constants.DISABLE_MAGIC_LOGIN = os.Getenv("DISABLE_MAGIC_LOGIN") constants.DISABLE_MAGIC_LOGIN = os.Getenv("DISABLE_MAGIC_LOGIN") == "true"
constants.JWT_ROLE_CLAIM = os.Getenv("JWT_ROLE_CLAIM") constants.JWT_ROLE_CLAIM = os.Getenv("JWT_ROLE_CLAIM")
if constants.ADMIN_SECRET == "" { if constants.ADMIN_SECRET == "" {
@@ -84,15 +87,30 @@ func InitEnv() {
allowedOriginsSplit := strings.Split(os.Getenv("ALLOWED_ORIGINS"), ",") allowedOriginsSplit := strings.Split(os.Getenv("ALLOWED_ORIGINS"), ",")
allowedOrigins := []string{} allowedOrigins := []string{}
hasWildCard := false
for _, val := range allowedOriginsSplit { for _, val := range allowedOriginsSplit {
trimVal := strings.TrimSpace(val) trimVal := strings.TrimSpace(val)
if trimVal != "" { if trimVal != "" {
allowedOrigins = append(allowedOrigins, trimVal) if trimVal != "*" {
host, port := utils.GetHostParts(trimVal)
allowedOrigins = append(allowedOrigins, host+":"+port)
} else {
hasWildCard = true
allowedOrigins = append(allowedOrigins, trimVal)
break
}
} }
} }
if len(allowedOrigins) > 1 && hasWildCard {
allowedOrigins = []string{"*"}
}
if len(allowedOrigins) == 0 { if len(allowedOrigins) == 0 {
allowedOrigins = []string{"*"} allowedOrigins = []string{"*"}
} }
constants.ALLOWED_ORIGINS = allowedOrigins constants.ALLOWED_ORIGINS = allowedOrigins
if *ARG_AUTHORIZER_URL != "" { if *ARG_AUTHORIZER_URL != "" {
@@ -115,6 +133,10 @@ func InitEnv() {
panic("Database type is required") panic("Database type is required")
} }
if constants.DATABASE_NAME == "" {
constants.DATABASE_NAME = "authorizer"
}
if constants.JWT_TYPE == "" { if constants.JWT_TYPE == "" {
constants.JWT_TYPE = "HS256" constants.JWT_TYPE = "HS256"
} }
@@ -123,20 +145,13 @@ func InitEnv() {
constants.COOKIE_NAME = "authorizer" constants.COOKIE_NAME = "authorizer"
} }
if constants.DISABLE_BASIC_AUTHENTICATION == "" { if constants.SMTP_HOST == "" || constants.SENDER_EMAIL == "" || constants.SENDER_PASSWORD == "" {
constants.DISABLE_BASIC_AUTHENTICATION = "false" constants.DISABLE_EMAIL_VERIFICATION = true
constants.DISABLE_MAGIC_LOGIN = true
} }
if constants.DISABLE_MAGIC_LOGIN == "" { if constants.DISABLE_EMAIL_VERIFICATION {
constants.DISABLE_MAGIC_LOGIN = "false" 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"
}
} }
rolesSplit := strings.Split(os.Getenv("ROLES"), ",") 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,35 @@ go 1.16
require ( require (
github.com/99designs/gqlgen v0.13.0 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/gin-gonic/gin v1.7.2
github.com/go-playground/validator/v10 v10.8.0 // indirect github.com/go-playground/validator/v10 v10.8.0 // indirect
github.com/go-redis/redis/v8 v8.11.0 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/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 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/joho/godotenv v1.3.0
github.com/json-iterator/go v1.1.11 // indirect github.com/json-iterator/go v1.1.11 // indirect
github.com/mattn/go-isatty v0.0.13 // 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/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f // 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/ugorji/go v1.2.6 // indirect
github.com/vektah/gqlparser/v2 v2.1.0 github.com/vektah/gqlparser/v2 v2.1.0
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 go.mongodb.org/mongo-driver v1.8.1 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gorm.io/driver/mysql v1.1.1 gorm.io/driver/mysql v1.2.1
gorm.io/driver/postgres v1.1.0 gorm.io/driver/postgres v1.2.3
gorm.io/driver/sqlite v1.1.4 gorm.io/driver/sqlite v1.2.6
gorm.io/gorm v1.21.11 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

@@ -49,7 +49,7 @@ func AppHandler() gin.HandlerFunc {
stateObj.RedirectURL = strings.TrimSuffix(stateObj.RedirectURL, "/") stateObj.RedirectURL = strings.TrimSuffix(stateObj.RedirectURL, "/")
// validate redirect url with allowed origins // validate redirect url with allowed origins
if !utils.IsValidRedirectURL(stateObj.RedirectURL) { if !utils.IsValidOrigin(stateObj.RedirectURL) {
c.JSON(400, gin.H{"error": "invalid redirect url"}) c.JSON(400, gin.H{"error": "invalid redirect url"})
return return
} }

View File

@@ -1,6 +1,7 @@
package handlers package handlers
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@@ -15,36 +16,50 @@ import (
"github.com/authorizerdev/authorizer/server/oauth" "github.com/authorizerdev/authorizer/server/oauth"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/session"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
func processGoogleUserInfo(code string) (db.User, error) { func processGoogleUserInfo(code string) (db.User, error) {
user := db.User{} 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 { if err != nil {
return user, fmt.Errorf("invalid google exchange code: %s", err.Error()) 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) verifier := oauth.OIDCProviders.GoogleOIDC.Verifier(&oidc.Config{ClientID: oauth.OAuthProviders.GoogleConfig.ClientID})
if err != nil {
return user, err // 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() // Parse and verify ID Token payload.
body, err := ioutil.ReadAll(response.Body) idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil { 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) // Extract custom claims
json.Unmarshal(body, &userRawData) 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{ user = db.User{
FirstName: userRawData["given_name"], FirstName: claims.GivenName,
LastName: userRawData["family_name"], LastName: claims.FamilyName,
Image: userRawData["picture"], Image: claims.Picture,
Email: userRawData["email"], Email: claims.Email,
EmailVerifiedAt: time.Now().Unix(), EmailVerifiedAt: time.Now().Unix(),
} }
@@ -53,7 +68,7 @@ func processGoogleUserInfo(code string) (db.User, error) {
func processGithubUserInfo(code string) (db.User, error) { func processGithubUserInfo(code string) (db.User, error) {
user := db.User{} user := db.User{}
token, err := oauth.OAuthProvider.GithubConfig.Exchange(oauth2.NoContext, code) token, err := oauth.OAuthProviders.GithubConfig.Exchange(oauth2.NoContext, code)
if err != nil { if err != nil {
return user, fmt.Errorf("invalid github exchange code: %s", err.Error()) 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) { func processFacebookUserInfo(code string) (db.User, error) {
user := db.User{} user := db.User{}
token, err := oauth.OAuthProvider.FacebookConfig.Exchange(oauth2.NoContext, code) token, err := oauth.OAuthProviders.FacebookConfig.Exchange(oauth2.NoContext, code)
if err != nil { if err != nil {
return user, fmt.Errorf("invalid facebook exchange code: %s", err.Error()) 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) response, err := client.Do(req)
if err != nil { if err != nil {
log.Println("err:", err) log.Println("error processing facebook user info:", err)
return user, err return user, err
} }
@@ -202,6 +217,7 @@ func OAuthCallbackHandler() gin.HandlerFunc {
} }
user.Roles = strings.Join(inputRoles, ",") user.Roles = strings.Join(inputRoles, ",")
user, _ = db.Mgr.AddUser(user)
} else { } else {
// user exists in db, check if method was google // user exists in db, check if method was google
// if not append google to existing signup method and save it // if not append google to existing signup method and save it
@@ -245,9 +261,12 @@ func OAuthCallbackHandler() gin.HandlerFunc {
} else { } else {
user.Roles = existingUser.Roles 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) user, _ = db.Mgr.GetUserByEmail(user.Email)
userIdStr := fmt.Sprintf("%v", user.ID) userIdStr := fmt.Sprintf("%v", user.ID)
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, inputRoles) refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, inputRoles)
@@ -262,7 +281,7 @@ func OAuthCallbackHandler() gin.HandlerFunc {
IP: utils.GetIP(c.Request), IP: utils.GetIP(c.Request),
} }
db.Mgr.SaveSession(sessionData) db.Mgr.AddSession(sessionData)
}() }()
c.Redirect(http.StatusTemporaryRedirect, redirectURL) c.Redirect(http.StatusTemporaryRedirect, redirectURL)

View File

@@ -48,28 +48,46 @@ func OAuthLoginHandler() gin.HandlerFunc {
oauthStateString := uuid.String() + "___" + redirectURL + "___" + roles oauthStateString := uuid.String() + "___" + redirectURL + "___" + roles
provider := c.Param("oauth_provider") provider := c.Param("oauth_provider")
isProviderConfigured := true
switch provider { switch provider {
case enum.Google.String(): case enum.Google.String():
if oauth.OAuthProviders.GoogleConfig == nil {
isProviderConfigured = false
break
}
session.SetSocailLoginState(oauthStateString, enum.Google.String()) session.SetSocailLoginState(oauthStateString, enum.Google.String())
// during the init of OAuthProvider authorizer url might be empty // during the init of OAuthProvider authorizer url might be empty
oauth.OAuthProvider.GoogleConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/google" oauth.OAuthProviders.GoogleConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/google"
url := oauth.OAuthProvider.GoogleConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.GoogleConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
case enum.Github.String(): case enum.Github.String():
if oauth.OAuthProviders.GithubConfig == nil {
isProviderConfigured = false
break
}
session.SetSocailLoginState(oauthStateString, enum.Github.String()) session.SetSocailLoginState(oauthStateString, enum.Github.String())
oauth.OAuthProvider.GithubConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/github" oauth.OAuthProviders.GithubConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/github"
url := oauth.OAuthProvider.GithubConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.GithubConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
case enum.Facebook.String(): case enum.Facebook.String():
if oauth.OAuthProviders.FacebookConfig == nil {
isProviderConfigured = false
break
}
session.SetSocailLoginState(oauthStateString, enum.Facebook.String()) session.SetSocailLoginState(oauthStateString, enum.Facebook.String())
oauth.OAuthProvider.FacebookConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/facebook" oauth.OAuthProviders.FacebookConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/facebook"
url := oauth.OAuthProvider.FacebookConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.FacebookConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
default: default:
c.JSON(422, gin.H{ c.JSON(422, gin.H{
"message": "Invalid oauth provider", "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 return
} }
_, err := db.Mgr.GetVerificationByToken(token) verificationRequest, err := db.Mgr.GetVerificationByToken(token)
if err != nil { if err != nil {
c.JSON(400, errorRes) c.JSON(400, errorRes)
return return
@@ -47,10 +47,11 @@ func VerifyEmailHandler() gin.HandlerFunc {
// update email_verified_at in users table // update email_verified_at in users table
if user.EmailVerifiedAt <= 0 { if user.EmailVerifiedAt <= 0 {
db.Mgr.UpdateVerificationTime(time.Now().Unix(), user.ID) user.EmailVerifiedAt = time.Now().Unix()
db.Mgr.UpdateUser(user)
} }
// delete from verification table // delete from verification table
db.Mgr.DeleteToken(claim.Email) db.Mgr.DeleteVerificationRequest(verificationRequest)
userIdStr := fmt.Sprintf("%v", user.ID) userIdStr := fmt.Sprintf("%v", user.ID)
roles := strings.Split(user.Roles, ",") roles := strings.Split(user.Roles, ",")
@@ -66,9 +67,9 @@ func VerifyEmailHandler() gin.HandlerFunc {
IP: utils.GetIP(c.Request), IP: utils.GetIP(c.Request),
} }
db.Mgr.SaveSession(sessionData) db.Mgr.AddSession(sessionData)
}() }()
utils.SetCookie(c, accessToken) utils.SetCookie(c, accessToken)
c.Redirect(http.StatusTemporaryRedirect, claim.Host) c.Redirect(http.StatusTemporaryRedirect, claim.RedirectURL)
} }
} }

View File

@@ -0,0 +1,44 @@
package integration_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/env"
"github.com/authorizerdev/authorizer/server/middlewares"
"github.com/gin-contrib/location"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestCors(t *testing.T) {
constants.ENV_PATH = "../../.env.local"
env.InitEnv()
r := gin.Default()
r.Use(location.Default())
r.Use(middlewares.GinContextToContextMiddleware())
r.Use(middlewares.CORSMiddleware())
allowedOrigin := "http://localhost:8080" // The allowed origin that you want to check
notAllowedOrigin := "http://myapp.com"
server := httptest.NewServer(r)
defer server.Close()
client := &http.Client{}
req, _ := http.NewRequest(
"GET",
"http://"+server.Listener.Addr().String()+"/api",
nil,
)
req.Header.Add("Origin", allowedOrigin)
get, _ := client.Do(req)
// You should get your origin (or a * depending on your config) if the
// passed origin is allowed.
o := get.Header.Get("Access-Control-Allow-Origin")
assert.NotEqual(t, o, notAllowedOrigin, "Origins should not match")
assert.Equal(t, o, allowedOrigin, "Origins don't match")
}

View File

@@ -1,12 +1,10 @@
package main package main
import ( import (
"context"
"log"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/env"
"github.com/authorizerdev/authorizer/server/handlers" "github.com/authorizerdev/authorizer/server/handlers"
"github.com/authorizerdev/authorizer/server/middlewares"
"github.com/authorizerdev/authorizer/server/oauth" "github.com/authorizerdev/authorizer/server/oauth"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/session"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
@@ -14,40 +12,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func GinContextToContextMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if constants.AUTHORIZER_URL == "" {
url := location.Get(c)
constants.AUTHORIZER_URL = url.Scheme + "://" + c.Request.Host
log.Println("=> setting url:", constants.AUTHORIZER_URL)
}
ctx := context.WithValue(c.Request.Context(), "GinContextKey", c)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
// TODO use allowed origins for cors origin
// TODO throw error if url is not allowed
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("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")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
func main() { func main() {
InitEnv() env.InitEnv()
db.InitDB() db.InitDB()
session.InitSession() session.InitSession()
oauth.InitOAuth() oauth.InitOAuth()
@@ -55,8 +21,8 @@ func main() {
r := gin.Default() r := gin.Default()
r.Use(location.Default()) r.Use(location.Default())
r.Use(GinContextToContextMiddleware()) r.Use(middlewares.GinContextToContextMiddleware())
r.Use(CORSMiddleware()) r.Use(middlewares.CORSMiddleware())
r.GET("/", handlers.PlaygroundHandler()) r.GET("/", handlers.PlaygroundHandler())
r.POST("/graphql", handlers.GraphqlHandler()) r.POST("/graphql", handlers.GraphqlHandler())

View File

@@ -0,0 +1,23 @@
package middlewares
import (
"context"
"log"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/gin-contrib/location"
"github.com/gin-gonic/gin"
)
func GinContextToContextMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if constants.AUTHORIZER_URL == "" {
url := location.Get(c)
constants.AUTHORIZER_URL = url.Scheme + "://" + c.Request.Host
log.Println("=> authorizer url:", constants.AUTHORIZER_URL)
}
ctx := context.WithValue(c.Request.Context(), "GinContextKey", c)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}

View File

@@ -0,0 +1,29 @@
package middlewares
import (
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/gin-gonic/gin"
)
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
constants.APP_URL = origin
if utils.IsValidOrigin(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")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}

View File

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

View File

@@ -69,7 +69,7 @@ func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*m
verificationType := enum.UpdateEmail.String() verificationType := enum.UpdateEmail.String()
token, err := utils.CreateVerificationToken(newEmail, verificationType) token, err := utils.CreateVerificationToken(newEmail, verificationType)
if err != nil { if err != nil {
log.Println(`Error generating token`, err) log.Println(`error generating token`, err)
} }
db.Mgr.AddVerification(db.VerificationRequest{ db.Mgr.AddVerification(db.VerificationRequest{
Token: token, Token: token,
@@ -110,7 +110,7 @@ func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*m
user, err = db.Mgr.UpdateUser(user) user, err = db.Mgr.UpdateUser(user)
if err != nil { if err != nil {
log.Println("Error updating user:", err) log.Println("error updating user:", err)
return res, err return res, err
} }

View File

@@ -29,9 +29,9 @@ func DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Respo
session.DeleteUserSession(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 { if err != nil {
log.Println("Err:", err) log.Println("error deleting user:", err)
return res, err return res, err
} }

View File

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

View File

@@ -22,7 +22,7 @@ func Login(ctx context.Context, params model.LoginInput) (*model.AuthResponse, e
return res, err 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`) 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)) err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(params.Password))
if err != nil { if err != nil {
log.Println("Compare password error:", err) log.Println("compare password error:", err)
return res, fmt.Errorf(`invalid password`) return res, fmt.Errorf(`invalid password`)
} }
roles := constants.DEFAULT_ROLES roles := constants.DEFAULT_ROLES
@@ -68,7 +68,7 @@ func Login(ctx context.Context, params model.LoginInput) (*model.AuthResponse, e
IP: utils.GetIP(gc.Request), IP: utils.GetIP(gc.Request),
} }
db.Mgr.SaveSession(sessionData) db.Mgr.AddSession(sessionData)
}() }()
res = &model.AuthResponse{ res = &model.AuthResponse{

View File

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

View File

@@ -17,7 +17,7 @@ import (
func MagicLogin(ctx context.Context, params model.MagicLoginInput) (*model.Response, error) { func MagicLogin(ctx context.Context, params model.MagicLoginInput) (*model.Response, error) {
var res *model.Response var res *model.Response
if constants.DISABLE_MAGIC_LOGIN == "true" { if constants.DISABLE_MAGIC_LOGIN {
return res, fmt.Errorf(`magic link login is disabled for this instance`) return res, fmt.Errorf(`magic link login is disabled for this instance`)
} }
@@ -35,6 +35,7 @@ func MagicLogin(ctx context.Context, params model.MagicLoginInput) (*model.Respo
// find user with email // find user with email
existingUser, err := db.Mgr.GetUserByEmail(params.Email) existingUser, err := db.Mgr.GetUserByEmail(params.Email)
if err != nil { if err != nil {
user.SignupMethod = enum.MagicLink.String() user.SignupMethod = enum.MagicLink.String()
// define roles for new user // define roles for new user
@@ -50,6 +51,7 @@ func MagicLogin(ctx context.Context, params model.MagicLoginInput) (*model.Respo
} }
user.Roles = strings.Join(inputRoles, ",") user.Roles = strings.Join(inputRoles, ",")
user, _ = db.Mgr.AddUser(user)
} else { } else {
user = existingUser user = existingUser
// There multiple scenarios with roles here in magic link login // There multiple scenarios with roles here in magic link login
@@ -90,16 +92,18 @@ func MagicLogin(ctx context.Context, params model.MagicLoginInput) (*model.Respo
} }
user.SignupMethod = signupMethod user.SignupMethod = signupMethod
user, _ = db.Mgr.UpdateUser(user)
if err != nil {
log.Println("error updating user:", err)
}
} }
user, _ = db.Mgr.SaveUser(user) if !constants.DISABLE_EMAIL_VERIFICATION {
if constants.DISABLE_EMAIL_VERIFICATION != "true" {
// insert verification request // insert verification request
verificationType := enum.MagicLink.String() verificationType := enum.MagicLink.String()
token, err := utils.CreateVerificationToken(params.Email, verificationType) token, err := utils.CreateVerificationToken(params.Email, verificationType)
if err != nil { if err != nil {
log.Println(`Error generating token`, err) log.Println(`error generating token`, err)
} }
db.Mgr.AddVerification(db.VerificationRequest{ db.Mgr.AddVerification(db.VerificationRequest{
Token: token, Token: token,
@@ -115,7 +119,7 @@ func MagicLogin(ctx context.Context, params model.MagicLoginInput) (*model.Respo
} }
res = &model.Response{ res = &model.Response{
Message: `Verification request has been sent. Please check your inbox!`, Message: `Magic Link has been sent to your email. Please check your inbox!`,
} }
return res, nil return res, nil

View File

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

View File

@@ -14,11 +14,11 @@ import (
func ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error) { func ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error) {
var res *model.Response 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`) 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 { if err != nil {
return res, fmt.Errorf(`invalid token`) return res, fmt.Errorf(`invalid token`)
} }
@@ -48,7 +48,7 @@ func ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model
user.SignupMethod = signupMethod user.SignupMethod = signupMethod
// delete from verification table // delete from verification table
db.Mgr.DeleteToken(claim.Email) db.Mgr.DeleteVerificationRequest(verificationRequest)
db.Mgr.UpdateUser(user) db.Mgr.UpdateUser(user)
res = &model.Response{ res = &model.Response{

View File

@@ -22,11 +22,11 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
return res, err 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`) return res, fmt.Errorf(`basic authentication is disabled for this instance`)
} }
if params.ConfirmPassword != params.Password { if params.ConfirmPassword != params.Password {
return res, fmt.Errorf(`passowrd and confirm password does not match`) return res, fmt.Errorf(`password and confirm password does not match`)
} }
params.Email = strings.ToLower(params.Email) params.Email = strings.ToLower(params.Email)
@@ -51,13 +51,16 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
// find user with email // find user with email
existingUser, err := db.Mgr.GetUserByEmail(params.Email) existingUser, err := db.Mgr.GetUserByEmail(params.Email)
if err != nil { 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 { if existingUser.EmailVerifiedAt > 0 {
// email is verified // email is verified
return res, fmt.Errorf(`you have already signed up. Please login`) return res, fmt.Errorf(`%s has already signed up`, params.Email)
} else if existingUser.ID != "" && existingUser.EmailVerifiedAt <= 0 {
return res, fmt.Errorf("%s has already signed up. please complete the email verification process or reset the password", params.Email)
} }
user := db.User{ user := db.User{
Email: params.Email, Email: params.Email,
} }
@@ -76,10 +79,10 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
} }
user.SignupMethod = enum.BasicAuth.String() user.SignupMethod = enum.BasicAuth.String()
if constants.DISABLE_EMAIL_VERIFICATION == "true" { if constants.DISABLE_EMAIL_VERIFICATION {
user.EmailVerifiedAt = time.Now().Unix() user.EmailVerifiedAt = time.Now().Unix()
} }
user, err = db.Mgr.SaveUser(user) user, err = db.Mgr.AddUser(user)
if err != nil { if err != nil {
return res, err return res, err
} }
@@ -98,12 +101,12 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
UpdatedAt: &user.UpdatedAt, UpdatedAt: &user.UpdatedAt,
} }
if constants.DISABLE_EMAIL_VERIFICATION != "true" { if !constants.DISABLE_EMAIL_VERIFICATION {
// insert verification request // insert verification request
verificationType := enum.BasicAuthSignup.String() verificationType := enum.BasicAuthSignup.String()
token, err := utils.CreateVerificationToken(params.Email, verificationType) token, err := utils.CreateVerificationToken(params.Email, verificationType)
if err != nil { if err != nil {
log.Println(`Error generating token`, err) log.Println(`error generating token`, err)
} }
db.Mgr.AddVerification(db.VerificationRequest{ db.Mgr.AddVerification(db.VerificationRequest{
Token: token, Token: token,
@@ -135,7 +138,7 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
IP: utils.GetIP(gc.Request), IP: utils.GetIP(gc.Request),
} }
db.Mgr.SaveSession(sessionData) db.Mgr.AddSession(sessionData)
}() }()
res = &model.AuthResponse{ res = &model.AuthResponse{
Message: `Signed up successfully.`, Message: `Signed up successfully.`,

View File

@@ -64,7 +64,7 @@ func Token(ctx context.Context, roles []string) (*model.AuthResponse, error) {
// if access token has expired and refresh/session token is valid // if access token has expired and refresh/session token is valid
// generate new accessToken // generate new accessToken
currentRefreshToken := session.GetToken(userIdStr, token) currentRefreshToken := session.GetToken(userIdStr, token)
session.DeleteToken(userIdStr, token) session.DeleteVerificationRequest(userIdStr, token)
token, expiresAt, _ = utils.CreateAuthToken(user, enum.AccessToken, claimRoles) token, expiresAt, _ = utils.CreateAuthToken(user, enum.AccessToken, claimRoles)
session.SetToken(userIdStr, token, currentRefreshToken) session.SetToken(userIdStr, token, currentRefreshToken)
go func() { go func() {
@@ -74,7 +74,7 @@ func Token(ctx context.Context, roles []string) (*model.AuthResponse, error) {
IP: utils.GetIP(gc.Request), IP: utils.GetIP(gc.Request),
} }
db.Mgr.SaveSession(sessionData) db.Mgr.AddSession(sessionData)
}() }()
} }
@@ -84,14 +84,16 @@ func Token(ctx context.Context, roles []string) (*model.AuthResponse, error) {
AccessToken: &token, AccessToken: &token,
AccessTokenExpiresAt: &expiresAt, AccessTokenExpiresAt: &expiresAt,
User: &model.User{ User: &model.User{
ID: userIdStr, ID: userIdStr,
Email: user.Email, Email: user.Email,
Image: &user.Image, Image: &user.Image,
FirstName: &user.FirstName, FirstName: &user.FirstName,
LastName: &user.LastName, LastName: &user.LastName,
Roles: strings.Split(user.Roles, ","), Roles: strings.Split(user.Roles, ","),
CreatedAt: &user.CreatedAt, CreatedAt: &user.CreatedAt,
UpdatedAt: &user.UpdatedAt, UpdatedAt: &user.UpdatedAt,
SignupMethod: user.SignupMethod,
EmailVerifiedAt: &user.EmailVerifiedAt,
}, },
} }
return res, nil return res, nil

View File

@@ -109,7 +109,7 @@ func UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model
verificationType := enum.UpdateEmail.String() verificationType := enum.UpdateEmail.String()
token, err := utils.CreateVerificationToken(newEmail, verificationType) token, err := utils.CreateVerificationToken(newEmail, verificationType)
if err != nil { if err != nil {
log.Println(`Error generating token`, err) log.Println(`error generating token`, err)
} }
db.Mgr.AddVerification(db.VerificationRequest{ db.Mgr.AddVerification(db.VerificationRequest{
Token: token, Token: token,
@@ -126,7 +126,7 @@ func UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model
_, err = db.Mgr.UpdateUser(user) _, err = db.Mgr.UpdateUser(user)
if err != nil { if err != nil {
log.Println("Error updating user:", err) log.Println("error updating user:", err)
return res, err return res, err
} }
message := `Profile details updated successfully.` message := `Profile details updated successfully.`

View File

@@ -20,7 +20,7 @@ func VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.Aut
return res, err return res, err
} }
_, err = db.Mgr.GetVerificationByToken(params.Token) verificationRequest, err := db.Mgr.GetVerificationByToken(params.Token)
if err != nil { if err != nil {
return res, fmt.Errorf(`invalid token`) 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 // 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 // delete from verification table
db.Mgr.DeleteToken(claim.Email) db.Mgr.DeleteVerificationRequest(verificationRequest)
userIdStr := fmt.Sprintf("%v", user.ID) userIdStr := fmt.Sprintf("%v", user.ID)
roles := strings.Split(user.Roles, ",") roles := strings.Split(user.Roles, ",")
@@ -55,7 +56,7 @@ func VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.Aut
IP: utils.GetIP(gc.Request), IP: utils.GetIP(gc.Request),
} }
db.Mgr.SaveSession(sessionData) db.Mgr.AddSession(sessionData)
}() }()
res = &model.AuthResponse{ res = &model.AuthResponse{

View File

@@ -1,7 +1,6 @@
package session package session
import ( import (
"log"
"sync" "sync"
) )
@@ -30,8 +29,6 @@ func (c *InMemoryStore) AddToken(userId, accessToken, refreshToken string) {
c.store[userId] = tempMap c.store[userId] = tempMap
} }
log.Println(c.store)
c.mu.Unlock() c.mu.Unlock()
} }
@@ -41,7 +38,7 @@ func (c *InMemoryStore) DeleteUserSession(userId string) {
c.mu.Unlock() c.mu.Unlock()
} }
func (c *InMemoryStore) DeleteToken(userId, accessToken string) { func (c *InMemoryStore) DeleteVerificationRequest(userId, accessToken string) {
c.mu.Lock() c.mu.Lock()
delete(c.store[userId], accessToken) delete(c.store[userId], accessToken)
c.mu.Unlock() c.mu.Unlock()

View File

@@ -29,7 +29,7 @@ func (c *RedisStore) DeleteUserSession(userId string) {
} }
} }
func (c *RedisStore) DeleteToken(userId, accessToken string) { func (c *RedisStore) DeleteVerificationRequest(userId, accessToken string) {
err := c.store.HDel(c.ctx, "authorizer_"+userId, accessToken).Err() err := c.store.HDel(c.ctx, "authorizer_"+userId, accessToken).Err()
if err != nil { if err != nil {
log.Fatalln("Error deleting redis token:", err) log.Fatalln("Error deleting redis token:", err)
@@ -47,7 +47,7 @@ func (c *RedisStore) GetToken(userId, accessToken string) string {
token := "" token := ""
res, err := c.store.HMGet(c.ctx, "authorizer_"+userId, accessToken).Result() res, err := c.store.HMGet(c.ctx, "authorizer_"+userId, accessToken).Result()
if err != nil { 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 { if len(res) > 0 && res[0] != nil {
token = fmt.Sprintf("%v", res[0]) token = fmt.Sprintf("%v", res[0])
@@ -66,7 +66,7 @@ func (c *RedisStore) GetSocialLoginState(key string) string {
state := "" state := ""
state, err := c.store.Get(c.ctx, key).Result() state, err := c.store.Get(c.ctx, key).Result()
if err != nil { if err != nil {
log.Println("Error getting token from redis store:", err) log.Println("error getting token from redis store:", err)
} }
return state return state

View File

@@ -27,12 +27,12 @@ func SetToken(userId, accessToken, refreshToken string) {
} }
} }
func DeleteToken(userId, accessToken string) { func DeleteVerificationRequest(userId, accessToken string) {
if SessionStoreObj.RedisMemoryStoreObj != nil { if SessionStoreObj.RedisMemoryStoreObj != nil {
SessionStoreObj.RedisMemoryStoreObj.DeleteToken(userId, accessToken) SessionStoreObj.RedisMemoryStoreObj.DeleteVerificationRequest(userId, accessToken)
} }
if SessionStoreObj.InMemoryStoreObj != nil { if SessionStoreObj.InMemoryStoreObj != nil {
SessionStoreObj.InMemoryStoreObj.DeleteToken(userId, accessToken) SessionStoreObj.InMemoryStoreObj.DeleteVerificationRequest(userId, accessToken)
} }
} }
@@ -96,7 +96,7 @@ func RemoveSocialLoginState(key string) {
func InitSession() { func InitSession() {
if constants.REDIS_URL != "" { 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) opt, err := redis.ParseURL(constants.REDIS_URL)
if err != nil { if err != nil {
log.Fatalln("Error parsing redis url:", err) log.Fatalln("Error parsing redis url:", err)
@@ -114,7 +114,7 @@ func InitSession() {
} }
} else { } else {
log.Println("Using in memory store to save sessions") log.Println("using in memory store to save sessions")
SessionStoreObj.InMemoryStoreObj = &InMemoryStore{ SessionStoreObj.InMemoryStoreObj = &InMemoryStore{
store: map[string]map[string]string{}, store: map[string]map[string]string{},
socialLoginState: map[string]string{}, socialLoginState: map[string]string{},

View File

@@ -64,12 +64,12 @@ func CreateAuthToken(user db.User, tokenType enum.TokenType, roles []string) (st
val, err := vm.Get("functionRes") val, err := vm.Get("functionRes")
if err != nil { if err != nil {
log.Println("=> err custom access token script:", err) log.Println("error getting custom access token script:", err)
} else { } else {
extraPayload := make(map[string]interface{}) extraPayload := make(map[string]interface{})
err = json.Unmarshal([]byte(fmt.Sprintf("%s", val)), &extraPayload) err = json.Unmarshal([]byte(fmt.Sprintf("%s", val)), &extraPayload)
if err != nil { if err != nil {
log.Println("Error converting accessTokenScript response to map:", err) log.Println("error converting accessTokenScript response to map:", err)
} else { } else {
for k, v := range extraPayload { for k, v := range extraPayload {
customClaims[k] = v customClaims[k] = v

View File

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

View File

@@ -1,30 +1,6 @@
package utils 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 // 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() { 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,8 +14,8 @@ func GetMetaInfo() model.Meta {
IsGithubLoginEnabled: constants.GITHUB_CLIENT_ID != "" && constants.GOOGLE_CLIENT_SECRET != "", IsGithubLoginEnabled: constants.GITHUB_CLIENT_ID != "" && constants.GOOGLE_CLIENT_SECRET != "",
IsFacebookLoginEnabled: constants.FACEBOOK_CLIENT_ID != "" && constants.FACEBOOK_CLIENT_SECRET != "", IsFacebookLoginEnabled: constants.FACEBOOK_CLIENT_ID != "" && constants.FACEBOOK_CLIENT_SECRET != "",
IsTwitterLoginEnabled: constants.TWITTER_CLIENT_ID != "" && constants.TWITTER_CLIENT_SECRET != "", IsTwitterLoginEnabled: constants.TWITTER_CLIENT_ID != "" && constants.TWITTER_CLIENT_SECRET != "",
IsBasicAuthenticationEnabled: constants.DISABLE_BASIC_AUTHENTICATION != "true", IsBasicAuthenticationEnabled: !constants.DISABLE_BASIC_AUTHENTICATION,
IsEmailVerificationEnabled: constants.DISABLE_EMAIL_VERIFICATION != "true", IsEmailVerificationEnabled: !constants.DISABLE_EMAIL_VERIFICATION,
IsMagicLoginEnabled: constants.DISABLE_MAGIC_LOGIN != "true", IsMagicLoginEnabled: !constants.DISABLE_MAGIC_LOGIN,
} }
} }

View File

@@ -5,21 +5,32 @@ import (
"strings" "strings"
) )
// function to get hostname // GetHostName function returns hostname and port
func GetHostName(auth_url string) string { func GetHostParts(uri string) (string, string) {
u, err := url.Parse(auth_url) tempURI := uri
if !strings.HasPrefix(tempURI, "http") && strings.HasPrefix(tempURI, "https") {
tempURI = "https://" + tempURI
}
u, err := url.Parse(tempURI)
if err != nil { if err != nil {
return `localhost` return "localhost", "8080"
} }
host := u.Hostname() host := u.Hostname()
port := u.Port()
return host return host, port
} }
// function to get domain name // GetDomainName function to get domain name
func GetDomainName(auth_url string) string { func GetDomainName(uri string) string {
u, err := url.Parse(auth_url) tempURI := uri
if !strings.HasPrefix(tempURI, "http") && strings.HasPrefix(tempURI, "https") {
tempURI = "https://" + tempURI
}
u, err := url.Parse(tempURI)
if err != nil { if err != nil {
return `localhost` return `localhost`
} }

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

@@ -0,0 +1,26 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetHostName(t *testing.T) {
authorizer_url := "http://test.herokuapp.com:80"
host, port := GetHostParts(authorizer_url)
expectedHost := "test.herokuapp.com"
assert.Equal(t, host, expectedHost, "hostname should be equal")
assert.Equal(t, port, "80", "port should be 80")
}
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

@@ -2,6 +2,7 @@ package utils
import ( import (
"net/mail" "net/mail"
"regexp"
"strings" "strings"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
@@ -13,16 +14,32 @@ func IsValidEmail(email string) bool {
return err == nil return err == nil
} }
func IsValidRedirectURL(url string) bool { func IsValidOrigin(url string) bool {
if len(constants.ALLOWED_ORIGINS) == 1 && constants.ALLOWED_ORIGINS[0] == "*" { if len(constants.ALLOWED_ORIGINS) == 1 && constants.ALLOWED_ORIGINS[0] == "*" {
return true return true
} }
hasValidURL := false hasValidURL := false
urlDomain := GetDomainName(url) hostName, port := GetHostParts(url)
currentOrigin := hostName + ":" + port
for _, val := range constants.ALLOWED_ORIGINS { for _, origin := range constants.ALLOWED_ORIGINS {
if strings.Contains(val, urlDomain) { replacedString := origin
// if has regex whitelisted domains
if strings.Contains(origin, "*") {
replacedString = strings.Replace(origin, ".", "\\.", -1)
replacedString = strings.Replace(replacedString, "*", ".*", -1)
if strings.HasPrefix(replacedString, ".*") {
replacedString += "\\b"
}
if strings.HasSuffix(replacedString, ".*") {
replacedString = "\\b" + replacedString
}
}
if matched, _ := regexp.MatchString(replacedString, currentOrigin); matched {
hasValidURL = true hasValidURL = true
break break
} }

View File

@@ -0,0 +1,34 @@
package utils
import (
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"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")
}
func TestIsValidOrigin(t *testing.T) {
// don't use portocal(http/https) for ALLOWED_ORIGINS while testing,
// as we trim them off while running the main function
constants.ALLOWED_ORIGINS = []string{"localhost:8080", "*.google.com", "*.google.in", "*abc.*"}
assert.False(t, IsValidOrigin("http://myapp.com"), "it should be invalid origin")
assert.False(t, IsValidOrigin("http://appgoogle.com"), "it should be invalid origin")
assert.True(t, IsValidOrigin("http://app.google.com"), "it should be valid origin")
assert.False(t, IsValidOrigin("http://app.google.ind"), "it should be invalid origin")
assert.True(t, IsValidOrigin("http://app.google.in"), "it should be valid origin")
assert.True(t, IsValidOrigin("http://xyx.abc.com"), "it should be valid origin")
assert.True(t, IsValidOrigin("http://xyx.abc.in"), "it should be valid origin")
assert.True(t, IsValidOrigin("http://xyxabc.in"), "it should be valid origin")
assert.True(t, IsValidOrigin("http://localhost:8080"), "it should be valid origin")
}

View File

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