feat: authenticate users and agents requests
This commit is contained in:
92
internal/auth/agent/authenticator.go
Normal file
92
internal/auth/agent/authenticator.go
Normal file
@ -0,0 +1,92 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/lestrrat-go/jwx/v2/jws"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
repo datastore.AgentRepository
|
||||
}
|
||||
|
||||
// Authenticate implements auth.Authenticator.
|
||||
func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth.User, error) {
|
||||
ctx = logger.With(r.Context(), logger.F("remoteAddr", r.RemoteAddr))
|
||||
|
||||
authorization := r.Header.Get("Authorization")
|
||||
if authorization == "" {
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
rawToken := strings.TrimPrefix(authorization, "Bearer ")
|
||||
if rawToken == "" {
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
token, err := jwt.Parse([]byte(rawToken), jwt.WithVerify(false))
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawThumbprint, exists := token.Get(keyThumbprint)
|
||||
if !exists {
|
||||
return nil, errors.Errorf("could not find '%s' claim", keyThumbprint)
|
||||
}
|
||||
|
||||
thumbrint, ok := rawThumbprint.(string)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected '%s' claim value: '%v'", keyThumbprint, rawThumbprint)
|
||||
}
|
||||
|
||||
agents, _, err := a.repo.Query(
|
||||
ctx,
|
||||
datastore.WithAgentQueryThumbprints(thumbrint),
|
||||
datastore.WithAgentQueryLimit(1),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(agents) != 1 {
|
||||
return nil, errors.Errorf("unexpected number of found agents: '%d'", len(agents))
|
||||
}
|
||||
|
||||
agent, err := a.repo.Get(
|
||||
ctx,
|
||||
agents[0].ID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
_, err = jwt.Parse(
|
||||
[]byte(rawToken),
|
||||
jwt.WithKeySet(agent.KeySet.Set, jws.WithRequireKid(false)),
|
||||
jwt.WithValidate(true),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
user := &User{
|
||||
agent: agent,
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func NewAuthenticator(repo datastore.AgentRepository) *Authenticator {
|
||||
return &Authenticator{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
var _ auth.Authenticator = &Authenticator{}
|
37
internal/auth/agent/jwt.go
Normal file
37
internal/auth/agent/jwt.go
Normal file
@ -0,0 +1,37 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const keyThumbprint = "thumbprint"
|
||||
|
||||
func GenerateToken(key jwk.Key, thumbprint string) (string, error) {
|
||||
token := jwt.New()
|
||||
|
||||
if err := token.Set(keyThumbprint, thumbprint); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := token.Set(jwt.IssuedAtKey, now); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawToken, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, key))
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return string(rawToken), nil
|
||||
}
|
23
internal/auth/agent/user.go
Normal file
23
internal/auth/agent/user.go
Normal file
@ -0,0 +1,23 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
agent *datastore.Agent
|
||||
}
|
||||
|
||||
// Subject implements auth.User
|
||||
func (u *User) Subject() string {
|
||||
return fmt.Sprintf("agent-%d", u.agent.ID)
|
||||
}
|
||||
|
||||
func (u *User) Agent() *datastore.Agent {
|
||||
return u.agent
|
||||
}
|
||||
|
||||
var _ auth.User = &User{}
|
82
internal/auth/middleware.go
Normal file
82
internal/auth/middleware.go
Normal file
@ -0,0 +1,82 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrCodeUnauthorized api.ErrorCode = "unauthorized"
|
||||
ErrCodeForbidden api.ErrorCode = "forbidden"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
contextKeyUser contextKey = "user"
|
||||
)
|
||||
|
||||
func CtxUser(ctx context.Context) (*User, error) {
|
||||
user, ok := ctx.Value(contextKeyUser).(*User)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected user type: expected '%T', got '%T'", new(User), ctx.Value(contextKeyUser))
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrUnauthenticated = errors.New("unauthenticated")
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
)
|
||||
|
||||
type User interface {
|
||||
Subject() string
|
||||
}
|
||||
|
||||
type Authenticator interface {
|
||||
Authenticate(context.Context, *http.Request) (User, error)
|
||||
}
|
||||
|
||||
func Middleware(authenticators ...Authenticator) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := logger.With(r.Context(), logger.F("remoteAddr", r.RemoteAddr))
|
||||
|
||||
var (
|
||||
user User
|
||||
err error
|
||||
)
|
||||
|
||||
for _, auth := range authenticators {
|
||||
user, err = auth.Authenticate(ctx, r)
|
||||
if err != nil {
|
||||
logger.Warn(ctx, "could not authenticate request", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
api.ErrorResponse(w, http.StatusUnauthorized, ErrCodeUnauthorized, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx = logger.With(ctx, logger.F("user", user.Subject()))
|
||||
ctx = context.WithValue(ctx, contextKeyUser, user)
|
||||
|
||||
h.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
67
internal/auth/user/authenticator.go
Normal file
67
internal/auth/user/authenticator.go
Normal file
@ -0,0 +1,67 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
keys jwk.Set
|
||||
issuer string
|
||||
}
|
||||
|
||||
// Authenticate implements auth.Authenticator.
|
||||
func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth.User, error) {
|
||||
ctx = logger.With(r.Context(), logger.F("remoteAddr", r.RemoteAddr))
|
||||
|
||||
authorization := r.Header.Get("Authorization")
|
||||
if authorization == "" {
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
rawToken := strings.TrimPrefix(authorization, "Bearer ")
|
||||
if rawToken == "" {
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
token, err := parseToken(ctx, a.keys, a.issuer, rawToken)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawRole, exists := token.Get(keyRole)
|
||||
if !exists {
|
||||
return nil, errors.New("could not find 'thumbprint' claim")
|
||||
}
|
||||
|
||||
role, ok := rawRole.(string)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected '%s' claim value: '%v'", keyRole, rawRole)
|
||||
}
|
||||
|
||||
if !isValidRole(role) {
|
||||
return nil, errors.Errorf("invalid role '%s'", role)
|
||||
}
|
||||
|
||||
user := &User{
|
||||
subject: token.Subject(),
|
||||
role: Role(role),
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func NewAuthenticator(keys jwk.Set, issuer string) *Authenticator {
|
||||
return &Authenticator{
|
||||
keys: keys,
|
||||
issuer: issuer,
|
||||
}
|
||||
}
|
||||
|
||||
var _ auth.Authenticator = &Authenticator{}
|
61
internal/auth/user/jwt.go
Normal file
61
internal/auth/user/jwt.go
Normal file
@ -0,0 +1,61 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jws"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const keyRole = "role"
|
||||
|
||||
func parseToken(ctx context.Context, keys jwk.Set, issuer string, rawToken string) (jwt.Token, error) {
|
||||
token, err := jwt.Parse(
|
||||
[]byte(rawToken),
|
||||
jwt.WithKeySet(keys, jws.WithRequireKid(false)),
|
||||
jwt.WithIssuer(issuer),
|
||||
jwt.WithValidate(true),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func GenerateToken(ctx context.Context, key jwk.Key, issuer, subject string, role Role) (string, error) {
|
||||
token := jwt.New()
|
||||
|
||||
if err := token.Set(jwt.SubjectKey, subject); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := token.Set(jwt.IssuerKey, issuer); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := token.Set(keyRole, role); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := token.Set(jwt.IssuedAtKey, now); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawToken, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, key))
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return string(rawToken), nil
|
||||
}
|
32
internal/auth/user/user.go
Normal file
32
internal/auth/user/user.go
Normal file
@ -0,0 +1,32 @@
|
||||
package user
|
||||
|
||||
import "forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleWriter Role = "writer"
|
||||
RoleReader Role = "reader"
|
||||
)
|
||||
|
||||
func isValidRole(r string) bool {
|
||||
rr := Role(r)
|
||||
|
||||
return rr == RoleWriter || rr == RoleReader
|
||||
}
|
||||
|
||||
type User struct {
|
||||
subject string
|
||||
role Role
|
||||
}
|
||||
|
||||
// Subject implements auth.User
|
||||
func (u *User) Subject() string {
|
||||
return u.subject
|
||||
}
|
||||
|
||||
func (u *User) Role() Role {
|
||||
return u.role
|
||||
}
|
||||
|
||||
var _ auth.User = &User{}
|
Reference in New Issue
Block a user