117 lines
3.2 KiB
Go
117 lines
3.2 KiB
Go
package interceptors
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"connectrpc.com/connect"
|
|
"git.kocoder.xyz/kocoded/vt/cmd/mailingest/query"
|
|
"git.kocoder.xyz/kocoded/vt/fx/interfaces/stores"
|
|
"git.kocoder.xyz/kocoded/vt/model"
|
|
"github.com/coreos/go-oidc/v3/oidc"
|
|
"gorm.io/gen/field"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const tokenHeader = "authentication"
|
|
const alternativeTokenHeader = "Authorization"
|
|
|
|
type AuthenticationInterceptor struct {
|
|
verifier *oidc.IDTokenVerifier
|
|
sr stores.SessionStore
|
|
logger *slog.Logger
|
|
db *gorm.DB
|
|
}
|
|
|
|
// WrapStreamingClient implements connect.Interceptor.
|
|
func (a *AuthenticationInterceptor) WrapStreamingClient(connect.StreamingClientFunc) connect.StreamingClientFunc {
|
|
panic("unimplemented")
|
|
}
|
|
|
|
// WrapStreamingHandler implements connect.Interceptor.
|
|
func (a *AuthenticationInterceptor) WrapStreamingHandler(connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {
|
|
panic("unimplemented")
|
|
}
|
|
|
|
// WrapUnary implements connect.Interceptor.
|
|
func (a *AuthenticationInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
|
|
return func(
|
|
ctx context.Context,
|
|
req connect.AnyRequest,
|
|
) (connect.AnyResponse, error) {
|
|
if req.Spec().IsClient {
|
|
panic(errors.New(""))
|
|
} else {
|
|
authToken := req.Header().Get(tokenHeader)
|
|
if authToken == "" {
|
|
authToken = req.Header().Get(alternativeTokenHeader)
|
|
}
|
|
|
|
if ok := strings.HasPrefix(authToken, "Bearer "); !ok {
|
|
a.logger.Info("No Authtoken with Bearer Prefix provided")
|
|
return nil, connect.NewError(
|
|
connect.CodeUnauthenticated,
|
|
errors.New("No valid token provided"),
|
|
)
|
|
}
|
|
authToken = strings.TrimPrefix(authToken, "Bearer ")
|
|
|
|
IDToken, err := a.verifier.Verify(context.TODO(), authToken)
|
|
if err != nil {
|
|
a.logger.Info("No valid/verifyable Authtoken provided", "err", err)
|
|
return nil, err
|
|
}
|
|
|
|
session, err := a.sr.GetSessionFromToken(IDToken.Subject)
|
|
|
|
if err != nil {
|
|
|
|
q := query.Use(a.db)
|
|
u := q.User
|
|
|
|
claims := model.User{}
|
|
|
|
if err := IDToken.Claims(&claims); err != nil {
|
|
a.logger.Error("Failed to parse claims from IDToken", "err", err)
|
|
panic(err)
|
|
}
|
|
|
|
a.logger.Info("Parsed Claims", "claims", claims)
|
|
|
|
us, err := u.Where(u.Sub.Eq(IDToken.Subject)).Assign(field.Attrs(&claims)).FirstOrCreate()
|
|
if err != nil {
|
|
a.logger.Error("Failed to get or create user from DB", "err", err, "sub", IDToken.Subject)
|
|
return nil, connect.NewError(connect.CodeInternal, errors.New("Failed to create user in DB"))
|
|
}
|
|
|
|
session = &stores.Session{Token: IDToken.Subject, UserID: us.ID, MandantId: 1}
|
|
a.sr.AddSession(session)
|
|
}
|
|
|
|
ctx = NewSessionContext(ctx, session)
|
|
}
|
|
|
|
return next(ctx, req)
|
|
}
|
|
}
|
|
|
|
func NewOIDCInterceptor(verifier *oidc.IDTokenVerifier, sr stores.SessionStore, logger *slog.Logger, db *gorm.DB) *AuthenticationInterceptor {
|
|
return &AuthenticationInterceptor{verifier: verifier, sr: sr, logger: logger, db: db}
|
|
}
|
|
|
|
type key int
|
|
|
|
var sessionKey key
|
|
|
|
func NewSessionContext(ctx context.Context, u *stores.Session) context.Context {
|
|
return context.WithValue(ctx, sessionKey, u)
|
|
}
|
|
|
|
// FromContext returns the User value stored in ctx, if any.
|
|
func SessionFromContext(ctx context.Context) (*stores.Session, bool) {
|
|
u, ok := ctx.Value(sessionKey).(*stores.Session)
|
|
return u, ok
|
|
}
|