WIPL tokens
This commit is contained in:
parent
c09c729157
commit
161e08fe1f
25 changed files with 716 additions and 63 deletions
90
internal/dis/component/auth/tokens/check.go
Normal file
90
internal/dis/component/auth/tokens/check.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package tokens
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
authHeader = "Authorization" // authorization
|
||||
authBearer = "Bearer" + " " // Prefix for bearer
|
||||
)
|
||||
|
||||
// TokenOf returns the token header found in the given request.
|
||||
// If r is nil, or there is no token, returns nil.
|
||||
// Error is only set if there is an error accessing the table that stores tokens.
|
||||
func (tok *Tokens) TokenOf(r *http.Request) (*models.Token, error) {
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// make sure that the authorization header exists and starts with the bearer
|
||||
auth := r.Header.Get(authHeader)
|
||||
if !strings.HasPrefix(auth, authBearer) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// get the token
|
||||
id := strings.TrimSpace(strings.TrimPrefix(auth, authBearer))
|
||||
if id == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
table, err := tok.table(r.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// take a single object from the tokens
|
||||
var tokenObj models.Token
|
||||
res := table.Where(&models.Token{Token: id}).Find(&tokenObj)
|
||||
|
||||
if res.Error != nil {
|
||||
return nil, errors.Join(ErrNoToken, res.Error)
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// and return the token object
|
||||
return &tokenObj, nil
|
||||
}
|
||||
|
||||
var ErrNoToken = errors.New("no token")
|
||||
|
||||
// Check checks if there is a token in the given request and if this request has an appropriate token with the appropriate scope.
|
||||
//
|
||||
// If the token is found and has the requested token, returns true, nil.
|
||||
// If there is a token found, but the specific scope is not set, returns false, nil.
|
||||
// If there is no valid authentication token found, returns false and an error that wraps ErrNoToken.
|
||||
// In other cases, other errors may be returned.
|
||||
//
|
||||
// Note that the scope may require an parameter to be validated.
|
||||
// This validation should take place in the appropriate ScopeProvider; which should recursively invoke this method.
|
||||
func (tok *Tokens) Check(r *http.Request, scope component.Scope) (bool, error) {
|
||||
// get the token object from the request
|
||||
tokenObj, err := tok.TokenOf(r)
|
||||
if tokenObj == nil {
|
||||
if err == nil {
|
||||
return false, ErrNoToken
|
||||
}
|
||||
return false, errors.Join(ErrNoToken, err)
|
||||
}
|
||||
|
||||
// TODO: Do we need this function?
|
||||
|
||||
// get the scopes
|
||||
scopes := tokenObj.GetScopes()
|
||||
if scopes == nil {
|
||||
// all scopes (implicitly)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// else check if they are contained
|
||||
return slices.Contains(scopes, string(scope)), nil
|
||||
}
|
||||
148
internal/dis/component/auth/tokens/tokens.go
Normal file
148
internal/dis/component/auth/tokens/tokens.go
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
package tokens
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/tkw1536/pkglib/password"
|
||||
"github.com/tkw1536/pkglib/reflectx"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Tokens implements Tokens
|
||||
type Tokens struct {
|
||||
component.Base
|
||||
|
||||
Dependencies struct {
|
||||
SQL *sql.SQL
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ component.UserDeleteHook = (*Tokens)(nil)
|
||||
_ component.Table = (*Tokens)(nil)
|
||||
)
|
||||
|
||||
func (tok *Tokens) TableInfo() component.TableInfo {
|
||||
return component.TableInfo{
|
||||
Name: models.TokensTable,
|
||||
Model: reflectx.MakeType[models.Token](),
|
||||
}
|
||||
}
|
||||
|
||||
func (tok *Tokens) table(ctx context.Context) (*gorm.DB, error) {
|
||||
return tok.Dependencies.SQL.QueryTable(ctx, tok)
|
||||
}
|
||||
|
||||
func (tok *Tokens) OnUserDelete(ctx context.Context, user *models.User) error {
|
||||
table, err := tok.table(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return table.Delete(&models.Token{}, &models.Token{User: user.User}).Error
|
||||
}
|
||||
|
||||
// Tokens returns a list of tokens for the given user
|
||||
func (tok *Tokens) Tokens(ctx context.Context, user string) ([]models.Token, error) {
|
||||
// the empty user has no tokens
|
||||
if user == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// get the table
|
||||
table, err := tok.table(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tokens []models.Token
|
||||
|
||||
// make a query to find all keys (in the underlying model)
|
||||
query := table.Find(&tokens, &models.Token{User: user})
|
||||
if query.Error != nil {
|
||||
return nil, query.Error
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
const (
|
||||
tokenGroupLength = 8
|
||||
tokenGroupCount = 8
|
||||
tokenSeparator = "-"
|
||||
tokenCharset password.Charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
)
|
||||
|
||||
// NewToken generates a new token
|
||||
func NewToken() (string, error) {
|
||||
// generate a new password
|
||||
token, err := password.Generate(rand.Reader, tokenGroupCount*tokenGroupLength, tokenCharset)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// insert the token group separators
|
||||
var result strings.Builder
|
||||
result.Grow(len(token) + (tokenGroupCount-1)*len(tokenSeparator))
|
||||
|
||||
for i := 0; i < tokenGroupCount; i++ {
|
||||
if i != 0 {
|
||||
result.WriteString(tokenSeparator)
|
||||
}
|
||||
|
||||
start := i * tokenGroupLength
|
||||
result.WriteString(token[start : start+tokenGroupLength])
|
||||
}
|
||||
|
||||
return result.String(), nil
|
||||
|
||||
}
|
||||
|
||||
// Add adds a new token, unless it already exists.
|
||||
// The token is granted scopes with .SetScopes(scopes).
|
||||
func (tok *Tokens) Add(ctx context.Context, user string, description string, scopes []string) (*models.Token, error) {
|
||||
|
||||
// create a new token and set the scopes
|
||||
mk := models.Token{
|
||||
User: user,
|
||||
Description: description,
|
||||
}
|
||||
mk.SetScopes(scopes)
|
||||
|
||||
// generate a new random password
|
||||
var err error
|
||||
mk.Token, err = NewToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get the table
|
||||
table, err := tok.table(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create the token instance
|
||||
if err := table.Create(&mk).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// and return
|
||||
return &mk, nil
|
||||
}
|
||||
|
||||
// Remove removes a token with the given token from the user
|
||||
func (tok *Tokens) Remove(ctx context.Context, user, token string) error {
|
||||
// get the table
|
||||
table, err := tok.table(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// and do the delete
|
||||
return table.Where("user = ? AND token = ?", user, token).Delete(&models.Token{}).Error
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue