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

Feature/login scopes refactoring #419

Merged
merged 3 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*.out
vendor
db.db
db_plugin.db
shared-local-instance.db
debug
*__debug_bin
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ build:
go build -o ./identifo

lint:
golangci-lint run -D deadcode,errcheck,unused,varcheck,govet
golangci-lint run -D errcheck,unused,govet

build_admin_panel:
rm -rf static/admin_panel
Expand Down
7 changes: 6 additions & 1 deletion cmd/config-boltdb.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ storage:
type: boltdb
boltdb:
path: ./db.db
userStorage: *storage_settings
userStorage:
type: plugin
plugin:
cmd: ./plugins/bin/bolt-user-storage
params: { "path": "./db_plugin.db" }
redirectStd: true
tokenStorage: *storage_settings
tokenBlacklist: *storage_settings
verificationCodeStorage: *storage_settings
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0
github.com/gorilla/sessions v1.2.1
github.com/hashicorp/go-hclog v0.14.1
github.com/hashicorp/go-plugin v1.4.5
github.com/hummerd/httpdump v0.9.1
github.com/joho/godotenv v1.4.0
Expand Down Expand Up @@ -70,7 +71,6 @@ require (
github.com/golang/snappy v0.0.1 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/hashicorp/go-hclog v0.14.1 // indirect
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/compress v1.13.6 // indirect
Expand Down
6 changes: 5 additions & 1 deletion impersonation/plugin/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import (
"os/exec"
"time"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
grpcShared "github.com/madappgang/identifo/v2/impersonation/grpc/shared"
"github.com/madappgang/identifo/v2/impersonation/plugin/shared"
"github.com/madappgang/identifo/v2/model"
)

func NewImpersonationProvider(settings model.PluginSettings, timeout time.Duration) (model.ImpersonationProvider, error) {
var err error
params := []string{}
for k, v := range settings.Params {
params = append(params, "-"+k)
Expand All @@ -24,6 +24,10 @@ func NewImpersonationProvider(settings model.PluginSettings, timeout time.Durati
Plugins: shared.PluginMap,
Cmd: exec.Command(settings.Cmd, params...),
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
Logger: hclog.New(&hclog.LoggerOptions{
Level: hclog.Debug,
JSONFormat: true,
}),
}

if settings.RedirectStd {
Expand Down
33 changes: 21 additions & 12 deletions jwt/service/jwt_token_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ func (ts *JWTokenService) ValidateTokenString(tstr string, v jwtValidator.Valida
// NewAccessToken creates new access token for user.
func (ts *JWTokenService) NewAccessToken(
user model.User,
scopes []string,
scopes model.AllowedScopesSet,
app model.AppData,
requireTFA bool,
tokenPayload map[string]interface{},
Expand All @@ -235,10 +235,11 @@ func (ts *JWTokenService) NewAccessToken(
payload[PayloadName] = user.Username
}

tokenType := model.TokenTypeAccess
scopesStr := scopes.String()
if requireTFA {
scopes = []string{model.TokenTypeTFAPreauth}
scopesStr = model.TokenTypeTFAPreauth
}

if len(tokenPayload) > 0 {
for k, v := range tokenPayload {
payload[k] = v
Expand All @@ -253,9 +254,9 @@ func (ts *JWTokenService) NewAccessToken(
}

claims := &model.Claims{
Scopes: strings.Join(scopes, " "),
Scopes: scopesStr,
Payload: payload,
Type: tokenType,
Type: model.TokenTypeAccess,
StandardClaims: jwt.StandardClaims{
ExpiresAt: (now + lifespan),
Issuer: ts.issuer,
Expand All @@ -278,22 +279,27 @@ func (ts *JWTokenService) NewAccessToken(
}

// NewRefreshToken creates new refresh token.
func (ts *JWTokenService) NewRefreshToken(u model.User, scopes []string, app model.AppData) (model.Token, error) {
func (ts *JWTokenService) NewRefreshToken(
user model.User,
scopes model.AllowedScopesSet,
app model.AppData,
) (model.Token, error) {
if !app.Active || !app.Offline {
return nil, ErrInvalidApp
}

// no offline request
if !model.SliceContains(scopes, model.OfflineScope) {
if !scopes.Contains(model.OfflineScope) {
return nil, ErrInvalidOfflineScope
}

if !u.Active {
if !user.Active {
return nil, ErrInvalidUser
}

payload := make(map[string]interface{})
if model.SliceContains(app.TokenPayload, PayloadName) {
payload[PayloadName] = u.Username
payload[PayloadName] = user.Username
}
now := ijwt.TimeFunc().Unix()

Expand All @@ -303,13 +309,13 @@ func (ts *JWTokenService) NewRefreshToken(u model.User, scopes []string, app mod
}

claims := &model.Claims{
Scopes: strings.Join(scopes, " "),
Scopes: scopes.String(),
Payload: payload,
Type: model.TokenTypeRefresh,
StandardClaims: jwt.StandardClaims{
ExpiresAt: (now + lifespan),
Issuer: ts.issuer,
Subject: u.ID,
Subject: user.ID,
Audience: app.ID,
IssuedAt: now,
},
Expand Down Expand Up @@ -338,7 +344,10 @@ func (ts *JWTokenService) NewRefreshToken(u model.User, scopes []string, app mod
}

// RefreshAccessToken issues new access token for provided refresh token.
func (ts *JWTokenService) RefreshAccessToken(refreshToken model.Token, tokenPayload map[string]interface{}) (model.Token, error) {
func (ts *JWTokenService) RefreshAccessToken(
refreshToken model.Token,
tokenPayload map[string]interface{},
) (model.Token, error) {
rt, ok := refreshToken.(*model.JWToken)
if !ok || rt == nil {
return nil, model.ErrTokenInvalid
Expand Down
6 changes: 5 additions & 1 deletion jwt/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func TestNewToken(t *testing.T) {
}, "password", "admin", false)
scopes := []string{"scope1", "scope2"}
tokenPayload := []string{"name"}

app := model.AppData{
ID: "123456",
Secret: "1",
Expand All @@ -138,7 +139,10 @@ func TestNewToken(t *testing.T) {
RolesBlacklist: []string{},
NewUserDefaultRole: "",
}
token, err := tokenService.NewAccessToken(user, scopes, app, false, nil)

allowedScopes := model.AllowedScopes(scopes, scopes, false)

token, err := tokenService.NewAccessToken(user, allowedScopes, app, false, nil)
assert.NoError(t, err)

tokenString, err := tokenService.String(token)
Expand Down
6 changes: 5 additions & 1 deletion model/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@ func SliceIntersect(a, b []string) []string {
}

func SliceContains(s []string, e string) bool {
el := strings.TrimSpace(e)

for _, a := range s {
if strings.TrimSpace(strings.ToLower(a)) == strings.TrimSpace(strings.ToLower(e)) {
if strings.EqualFold(strings.TrimSpace(a), el) {
return true
}

}

return false
}

Expand Down
33 changes: 25 additions & 8 deletions model/token.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package model

import (
"strings"
"time"

jwt "github.com/golang-jwt/jwt/v4"
Expand Down Expand Up @@ -182,15 +183,31 @@ type Claims struct {
// Full example of how to use JWT tokens:
// https://github.com/form3tech-oss/jwt-go/blob/master/cmd/jwt/app.go

func AllowedScopes(requestedScopes, userScopes []string, isOffline bool) []string {
scopes := []string{}
// This type is needed for guard against passing unchecked scopes to the token.
// Do not convert user provided scopes to this type directly.
type AllowedScopesSet struct {
scopes []string
}

func (a AllowedScopesSet) String() string {
return strings.Join(a.scopes, " ")
}

func (a AllowedScopesSet) Scopes() []string {
return a.scopes
}

func (a AllowedScopesSet) Contains(scope string) bool {
return SliceContains(a.scopes, scope)
}

func AllowedScopes(requestedScopes, userScopes []string, isOffline bool) AllowedScopesSet {
// if we requested any scope, let's provide all the scopes user has and requested
if len(requestedScopes) > 0 {
scopes = SliceIntersect(requestedScopes, userScopes)
}
if SliceContains(requestedScopes, "offline") && isOffline {
scopes = append(scopes, "offline")
scopes := SliceIntersect(requestedScopes, userScopes)

if SliceContains(requestedScopes, OfflineScope) && isOffline {
scopes = append(scopes, OfflineScope)
}

return scopes
return AllowedScopesSet{scopes}
}
4 changes: 2 additions & 2 deletions model/token_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ const (

// TokenService is an abstract token manager.
type TokenService interface {
NewAccessToken(u User, scopes []string, app AppData, requireTFA bool, tokenPayload map[string]interface{}) (Token, error)
NewRefreshToken(u User, scopes []string, app AppData) (Token, error)
NewAccessToken(u User, scopes AllowedScopesSet, app AppData, requireTFA bool, tokenPayload map[string]interface{}) (Token, error)
NewRefreshToken(u User, scopes AllowedScopesSet, app AppData) (Token, error)
RefreshAccessToken(token Token, tokenPayload map[string]interface{}) (Token, error)
NewInviteToken(email, role, audience string, data map[string]interface{}) (Token, error)
NewResetToken(userID string) (Token, error)
Expand Down
15 changes: 14 additions & 1 deletion plugins/bolt-user-storage/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"flag"
"log/slog"
"os"
"os/signal"
"syscall"
Expand All @@ -13,7 +14,20 @@ import (
"github.com/madappgang/identifo/v2/storage/plugin/shared"
)

type wproxy struct {
}

func (w wproxy) Write(p []byte) (n int, err error) {
return os.Stderr.Write(p)
}

func main() {
slog.SetDefault(slog.New(slog.NewJSONHandler(
wproxy{},
&slog.HandlerOptions{
Level: slog.LevelDebug,
})))

path := flag.String("path", "", "path to database")
flag.Parse()

Expand All @@ -34,7 +48,6 @@ func main() {
Plugins: map[string]plugin.Plugin{
"user-storage": &shared.UserStoragePlugin{Impl: s},
},

// A non-nil value here enables gRPC serving for this plugin...
GRPCServer: plugin.DefaultGRPCServer,
})
Expand Down
9 changes: 7 additions & 2 deletions storage/dynamodb/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,17 @@ func (db *DB) IsTableExists(table string) (bool, error) {
}

func (db *DB) DeleteTable(table string) error {
svc := dynamodb.New(session.New())
sess, err := session.NewSession()
if err != nil {
return err
}

svc := dynamodb.New(sess)
input := &dynamodb.DeleteTableInput{
TableName: aws.String(table),
}

_, err := svc.DeleteTable(input)
_, err = svc.DeleteTable(input)
return err
}

Expand Down
1 change: 1 addition & 0 deletions storage/dynamodb/invite.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ func (is *InviteStorage) ArchiveAllByEmail(email string) error {
if err != nil {
is.logger.Error("Error querying for invites",
logging.FieldError, err)

return ErrorInternalError
}

Expand Down
1 change: 1 addition & 0 deletions storage/dynamodb/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ func (us *UserStorage) UserByID(id string) (model.User, error) {
if err != nil {
us.logger.Error("Error getting item from DynamoDB",
logging.FieldError, err)

return model.User{}, ErrorInternalError
}
if result.Item == nil {
Expand Down
6 changes: 5 additions & 1 deletion storage/plugin/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"
"os/exec"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/madappgang/identifo/v2/model"
grpcShared "github.com/madappgang/identifo/v2/storage/grpc/shared"
Expand All @@ -12,7 +13,6 @@ import (

// NewUserStorage creates and inits plugin user storage.
func NewUserStorage(settings model.PluginSettings) (model.UserStorage, error) {
var err error
params := []string{}
for k, v := range settings.Params {
params = append(params, "-"+k)
Expand All @@ -24,6 +24,10 @@ func NewUserStorage(settings model.PluginSettings) (model.UserStorage, error) {
Plugins: shared.PluginMap,
Cmd: exec.Command(settings.Cmd, params...),
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
Logger: hclog.New(&hclog.LoggerOptions{
Level: hclog.Debug,
JSONFormat: true,
}),
}

if settings.RedirectStd {
Expand Down
7 changes: 5 additions & 2 deletions user_payload_provider/plugin/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os/exec"
"time"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/madappgang/identifo/v2/model"
grpcShared "github.com/madappgang/identifo/v2/user_payload_provider/grpc/shared"
Expand All @@ -13,19 +14,21 @@ import (

// NewTokenPayloadProvider creates and inits plugin for payload provider.
func NewTokenPayloadProvider(settings model.PluginSettings, timeout time.Duration) (model.TokenPayloadProvider, error) {
var err error
params := []string{}
for k, v := range settings.Params {
params = append(params, "-"+k)
params = append(params, v)
}

cfg := &plugin.ClientConfig{
SyncStdout: os.Stdout,
HandshakeConfig: shared.Handshake,
Plugins: shared.PluginMap,
Cmd: exec.Command(settings.Cmd, params...),
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
Logger: hclog.New(&hclog.LoggerOptions{
Level: hclog.Debug,
JSONFormat: true,
}),
}

if settings.RedirectStd {
Expand Down
Loading
Loading