package api import ( "context" "database/sql" "errors" "fmt" "log/slog" "net/http" "strings" "time" "atlas9.dev/c/core" "atlas9.dev/c/core/dbi" "atlas9.dev/c/core/iam" "atlas9.dev/c/demo/bots" "atlas9.dev/c/demo/lib" ) type IamMiddleware struct { DB *sql.DB Access dbi.Factory[lib.AccessStore] Sessions dbi.Factory[iam.SessionStore] Keys dbi.Factory[bots.KeyStore] SessionMan *iam.SessionMan } func (i *IamMiddleware) Handler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var principalID core.ID var access lib.Access err := dbi.ReadOnly(ctx, i.DB, func(tx dbi.DBI) error { // Try to load a user session. if cookie, _ := r.Cookie(i.SessionMan.CookieName); cookie != nil { s, err := i.SessionMan.LoadSession(ctx, tx, cookie.Value) switch { case err == core.ErrNotFound: case err != nil: return err default: principalID = s.Principal } } // If principal is still empty, then try to load a bot identity. if principalID.IsEmpty() { b, err := loadBot(ctx, r, i.Keys(tx)) if err != nil { return err } principalID = b } // If principal is still empty, then the request is anonymous. if principalID.IsEmpty() { return nil } access.Authenticated() return i.Access(tx).Load(ctx, principalID, &access) }) if errors.Is(err, iam.ErrUnauthorized) { i.SessionMan.ClearCookie(w) http.Redirect(w, r, "/login", http.StatusFound) return } if writeErr(ctx, w, err) { return } ctx = iam.PutPrincipal(ctx, iam.Principal{Subject: principalID.String()}) ctx = lib.PutAccess(ctx, access) r = r.WithContext(ctx) next.ServeHTTP(w, r) }) } func loadBot(ctx context.Context, r *http.Request, keys bots.KeyStore) (core.ID, error) { // TODO support Bearer. Caller can decide signature vs bearer. // No valid session, try API key signature authHeader := r.Header.Get("Authorization") if !strings.HasPrefix(authHeader, "Signature ") { return core.ID{}, nil } var keyID core.ID var tenantID core.ID // Get the API key var key bots.Key err := keys.Get(ctx, keyID, tenantID, &key) if errors.Is(err, bots.ErrKeyNotFound) { slog.Info("api key not found", "ID", keyID) return core.ID{}, iam.ErrUnauthorized } if err != nil { return core.ID{}, fmt.Errorf("reading api key: %w", err) } // Check if key is expired if time.Now().After(key.ExpiresAt) { slog.Info("api key expired", "ID", keyID, "ExpiresAt", key.ExpiresAt) return core.ID{}, iam.ErrUnauthorized } slog.Info("api key accepted", "ID", key.ID, "Bot", key.Bot, "Tenant", key.Tenant.String(), ) // Set principal to the bot ID return key.Bot, nil } func verifySig() { // // Verify signature // // Get the signed string from Signature-Input header // sigInputHeader := r.Header.Get("Signature-Input") // signedStrings, err := apikey.ParseSignatureInputHeader(sigInputHeader) // if err != nil { // slog.Debug("invalid signature-input header", "err", err) // http.Error(w, "unauthorized", http.StatusUnauthorized) // return // } // signedString, ok := signedStrings["sig1"] // if !ok { // slog.Debug("missing sig1 in signature-input") // http.Error(w, "unauthorized", http.StatusUnauthorized) // return // } // // Get the signature value // signature, ok := sigParams["signature"] // if !ok { // slog.Debug("missing signature parameter") // http.Error(w, "unauthorized", http.StatusUnauthorized) // return // } // // Verify the signature // err = apikey.VerifySignature(signedString, signature, key.SecretHash) // if err != nil { // slog.Debug("signature verification failed", "err", err) // http.Error(w, "unauthorized", http.StatusUnauthorized) // return // } }