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 }