Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support additional authentication methods - Oauth #29

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0ecc411
testing
CubicrootXYZ May 4, 2021
de3f94f
remove testing
CubicrootXYZ May 4, 2021
2c4400e
add PoC for oauth2
CubicrootXYZ May 9, 2021
b79b946
add password anmd user check
CubicrootXYZ May 9, 2021
a247db1
started moving oauth to current authentication
CubicrootXYZ May 11, 2021
7c5ce31
integrate oauth in current auth
CubicrootXYZ May 13, 2021
609eb66
move things again and introduce jwt tokens
CubicrootXYZ May 17, 2021
6ee285d
move to authhandler
CubicrootXYZ May 22, 2021
2dce377
get /auth to work properly
CubicrootXYZ May 23, 2021
e78ffda
stack authentication layers & add documentation
CubicrootXYZ May 23, 2021
f60bb83
add refresh config
CubicrootXYZ May 23, 2021
46027e2
clean up & add file support
CubicrootXYZ May 23, 2021
fc6bc82
move token key to config & clean up
CubicrootXYZ May 30, 2021
393d586
removed replaced code
CubicrootXYZ May 30, 2021
69970e4
Merge branch 'pushbits:master' into oauth-testing
CubicrootXYZ May 30, 2021
cee8001
clean up errors
CubicrootXYZ Jun 2, 2021
0504528
wording & consistency
CubicrootXYZ Jun 3, 2021
364e522
panic when no oauth secrets are set
CubicrootXYZ Jun 3, 2021
a06fe24
clean up & consistency
CubicrootXYZ Jun 3, 2021
1e2114b
register auth handler
CubicrootXYZ Jun 3, 2021
43b2908
move to POST
CubicrootXYZ Jun 3, 2021
48b4f71
added longterm tokens
CubicrootXYZ Jun 5, 2021
5b0c124
use body data for auth
CubicrootXYZ Jun 5, 2021
488f426
reflect changes in readme
CubicrootXYZ Jun 5, 2021
c92b783
let oauth use existing db
CubicrootXYZ Jun 19, 2021
1a1ee00
Merge branch 'master' into oauth-testing
CubicrootXYZ Jul 21, 2021
a5e6472
clean up merge conflicts
CubicrootXYZ Jul 21, 2021
b226a5e
add missing package
CubicrootXYZ Jul 21, 2021
d98aa1c
lowercase errors
CubicrootXYZ Jul 21, 2021
bed622e
allow multiple oauth clients
CubicrootXYZ Sep 12, 2021
4fe8d7f
Merge branch 'master' into oauth-testing
CubicrootXYZ Nov 21, 2021
2c62a49
add ginserver again
CubicrootXYZ Nov 21, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
started moving oauth to current authentication
CubicrootXYZ committed May 11, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit a247db10996471435771527a826c52a19295ab89
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -137,7 +137,7 @@ security:
For [basic authentication](https://de.wikipedia.org/wiki/HTTP-Authentifizierung) you have to provide your username and password in each request to the server. For example in curl you can do this with the `--user` flag:

```bash
curl --username myusername:totalysecretpassword
curl -u myusername:totalysecretpassword
```

#### Oauth 2
2 changes: 1 addition & 1 deletion cmd/pushbits/main.go
Original file line number Diff line number Diff line change
@@ -60,7 +60,7 @@ func main() {
log.Fatal(err)
}

engine := router.Create(c.Debug, cm, db, dp, c.Security.Authentication)
engine := router.Create(c.Debug, cm, db, dp, c.Authentication)

runner.Run(engine, c.HTTP.ListenAddress, c.HTTP.Port)
}
11 changes: 9 additions & 2 deletions config.example.yml
Original file line number Diff line number Diff line change
@@ -47,8 +47,6 @@ matrix:
security:
# Wether or not to check for weak passwords using HIBP.
checkhibp: false
# The authentication method to use
authentication: basic

crypto:
# Configuration of the KDF for password storage. Do not change unless you know what you are doing!
@@ -62,3 +60,12 @@ crypto:
formatting:
# Whether to use colored titles based on the message priority (<0: grey, 0-3: default, 4-10: yellow, 10-20: orange, >20: red).
coloredtitle: false

authentication:
# The authentication method to use
method: basic
oauth:
# The storage used for tokens
storage: "file"
# The connection to the storage
connection: "pushbits_token.db"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to have a separate file for the tokens? sqlite is already an option for the user data, guess we can have a new table in the same file?

19 changes: 19 additions & 0 deletions internal/api/middleware.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package api

import (
"fmt"
"log"

"github.com/gin-gonic/gin"
"gopkg.in/oauth2.v3"

ginserver "github.com/go-oauth2/gin-server"
)

type idInURI struct {
@@ -20,3 +26,16 @@ func RequireIDInURI() gin.HandlerFunc {
ctx.Set("id", requestModel.ID)
}
}

// RequireIDFromToken returns a Gin middleware which requires an ID to be supplied by the oauth token
func RequireIDFromToken() gin.HandlerFunc {
return func(ctx *gin.Context) {
ti, exists := ctx.Get(ginserver.DefaultConfig.TokenKey)
ti2, ok := ti.(oauth2.TokenInfo)
log.Println(fmt.Sprintf("USER ID: %s", ti2.GetUserID()))
if exists && ok {
ctx.Set("id", ti2.GetUserID)
return
}
}
}
56 changes: 54 additions & 2 deletions internal/authentication/authentication.go
Original file line number Diff line number Diff line change
@@ -3,9 +3,13 @@ package authentication
import (
"errors"
"net/http"
"strconv"

ginserver "github.com/go-oauth2/gin-server"
"github.com/pushbits/server/internal/authentication/credentials"
"github.com/pushbits/server/internal/configuration"
"github.com/pushbits/server/internal/model"
"gopkg.in/oauth2.v3"

"github.com/gin-gonic/gin"
)
@@ -18,11 +22,13 @@ const (
type Database interface {
GetApplicationByToken(token string) (*model.Application, error)
GetUserByName(name string) (*model.User, error)
GetUserByID(id uint) (*model.User, error)
}

// Authenticator is the provider for authentication middleware.
type Authenticator struct {
DB Database
DB Database
Config configuration.Authentication
}

type hasUserProperty func(user *model.User) bool
@@ -41,9 +47,42 @@ func (a *Authenticator) userFromBasicAuth(ctx *gin.Context) (*model.User, error)
return nil, errors.New("no credentials were supplied")
}

func (a *Authenticator) userFromToken(ctx *gin.Context) (*model.User, error) {
ti, exists := ctx.Get(ginserver.DefaultConfig.TokenKey)
if !exists {
return nil, errors.New("No token available")
}

token, ok := ti.(oauth2.TokenInfo)
if !ok {
return nil, errors.New("Wrong token format")
}

userID, err := strconv.ParseUint(token.GetUserID(), 10, 64)
if err != nil {
return nil, errors.New("User information of wrong format")
}

user, err := a.DB.GetUserByID(uint(userID))
if err != nil {
return nil, err
}

return user, nil
}

func (a *Authenticator) requireUserProperty(has hasUserProperty) gin.HandlerFunc {
return func(ctx *gin.Context) {
user, err := a.userFromBasicAuth(ctx)
var user *model.User
err := errors.New("No authentication method")

switch a.Config.Method {
case "oauth":
user, err = a.userFromToken(ctx)
default:
user, err = a.userFromBasicAuth(ctx)
}

if err != nil {
ctx.AbortWithError(http.StatusForbidden, err)
return
@@ -104,3 +143,16 @@ func (a *Authenticator) RequireApplicationToken() gin.HandlerFunc {
ctx.Set("app", app)
}
}

// RequireValidAuthentication returns a Gin middleware which requires a valid authentication
func (a *Authenticator) RequireValidAuthentication() gin.HandlerFunc {
switch a.Config.Method {
case "oauth":
return ginserver.HandleTokenVerify() // TODO cubicroot move to own config and set error handler to display same errors as for basic auth
default:
// TODO cubicroot: not very nice to have duplicated code here - but we need the HandleTokenVerify somewhere
return a.requireUserProperty(func(user *model.User) bool {
return true
})
}
}
45 changes: 27 additions & 18 deletions internal/authentication/oauth/init.go
Original file line number Diff line number Diff line change
@@ -4,40 +4,49 @@ import (
ginserver "github.com/go-oauth2/gin-server"
mysql "github.com/imrenagi/go-oauth2-mysql"
"github.com/jmoiron/sqlx"

"github.com/pushbits/server/internal/configuration"
"github.com/pushbits/server/internal/database"

"gopkg.in/oauth2.v3/manage"
"gopkg.in/oauth2.v3/models"
"gopkg.in/oauth2.v3/server"

"errors"
"log"
)

// InitializeOauth sets up the basics for oauth authentication
func InitializeOauth(db *database.Database) error {
// Initialize the database
dbOauth, err := sqlx.Connect("mysql", "?parseTime=true") // TODO cubicroot add more options and move to settings
if err != nil {
log.Fatal(err)
func InitializeOauth(db *database.Database, config configuration.Authentication) error {
// TODO cubicroot move that to the authenticator?
manager := manage.NewDefaultManager()

if config.Oauth.Storage == "mysql" {
dbOauth, err := sqlx.Connect("mysql", config.Oauth.Connection+"?parseTime=true") // TODO cubicroot add more options and move to settings
if err != nil {
log.Fatal(err)
}

manager.MustTokenStorage(mysql.NewTokenStore(dbOauth))

clientStore, _ := mysql.NewClientStore(dbOauth, mysql.WithClientStoreTableName("oauth_clients"))
manager.MapClientStorage(clientStore)

// TODO cubicroot better only store the secret as hashed value and autogenerate?
clientStore.Create(&models.Client{
ID: "000000",
Secret: "999999",
Domain: "http://localhost",
})
} else {
// TODO cubicroot add more storage options
return errors.New("Unknown oauth storage")
}

auth := Authenticator{
DB: db,
}

manager := manage.NewDefaultManager()
manager.MustTokenStorage(mysql.NewTokenStore(dbOauth))

clientStore, _ := mysql.NewClientStore(dbOauth, mysql.WithClientStoreTableName("oauth_clients"))
manager.MapClientStorage(clientStore)

// TODO cubicroot move to settings
clientStore.Create(&models.Client{
ID: "000000",
Secret: "999999",
Domain: "http://localhost",
})

ginserver.InitServer(manager)
ginserver.SetAllowGetAccessRequest(true)
ginserver.SetClientInfoHandler(server.ClientFormHandler)
20 changes: 16 additions & 4 deletions internal/configuration/configuration.go
Original file line number Diff line number Diff line change
@@ -23,6 +23,18 @@ type Formatting struct {
ColoredTitle bool `default:"false"`
}

// Authentication holds the settings for the oauth server
type Authentication struct {
Method string `default:"basic"`
Oauth Oauth
}

// Oauth holds information about the oauth server
type Oauth struct {
Connection string `default:""`
Storage string `default:"file"`
}

// Configuration holds values that can be configured by the user.
type Configuration struct {
Debug bool `default:"false"`
@@ -45,11 +57,11 @@ type Configuration struct {
Password string `required:"true"`
}
Security struct {
CheckHIBP bool `default:"false"`
Authentication string `default:"basic"`
CheckHIBP bool `default:"false"`
}
Crypto CryptoConfig
Formatting Formatting
Crypto CryptoConfig
Formatting Formatting
Authentication Authentication
}

func configFiles() []string {
21 changes: 14 additions & 7 deletions internal/router/router.go
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import (
"github.com/pushbits/server/internal/authentication"
"github.com/pushbits/server/internal/authentication/credentials"
"github.com/pushbits/server/internal/authentication/oauth"
"github.com/pushbits/server/internal/configuration"
"github.com/pushbits/server/internal/database"
"github.com/pushbits/server/internal/dispatcher"

@@ -16,14 +17,17 @@ import (
)

// Create a Gin engine and setup all routes.
func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher, authMethod string) *gin.Engine {
func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher, authConfig configuration.Authentication) *gin.Engine {
log.Println("Setting up HTTP routes.")

if !debug {
gin.SetMode(gin.ReleaseMode)
}

auth := authentication.Authenticator{DB: db}
auth := authentication.Authenticator{
DB: db,
Config: authConfig,
}

applicationHandler := api.ApplicationHandler{DB: db, DP: dp}
healthHandler := api.HealthHandler{DB: db}
@@ -36,8 +40,8 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp
// Example from the library: https://github.com/go-oauth2/oauth2/blob/master/example/server/server.go
// Good Tutorial: https://tutorialedge.net/golang/go-oauth2-tutorial/

if authMethod == "oauth" {
oauth.InitializeOauth(db)
if authConfig.Method == "oauth" {
oauth.InitializeOauth(db, authConfig)

oauthGroup := r.Group("/oauth2")
{
@@ -48,17 +52,20 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp
}

// TODO cubicroot remove - currently only for testing
api := r.Group("/oauthtest")
oauthtest := r.Group("/oauthtest")

oauthtest.Use(auth.RequireValidAuthentication())
oauthtest.Use(auth.RequireUser())
{
api.Use(ginserver.HandleTokenVerify())
api.GET("/info", func(c *gin.Context) {
oauthtest.GET("/info", func(c *gin.Context) {
ti, exists := c.Get(ginserver.DefaultConfig.TokenKey)
if exists {
c.JSON(200, ti)
return
}
c.String(200, "not found")
})
oauthtest.GET("", api.RequireIDFromToken(), applicationHandler.GetApplications)
}
} else {
// TODO cubicroot add other auth methods here