LINUS ROUND 4: RS256 auth + ouroboros-identity integration

- auth/rs256.go: RS256 JWT validation with AAMOS public key
- auth/rs256_test.go: 4 RS256 tests (success, invalid sig, expired, HS256 reject)
- auth/integration_test.go: Real AAMOS identity service integration test
- Copied jwt-public.pem from /opt/amos/data/keys/
- ouroboros-identity running on port 3208
This commit is contained in:
Bernt (LandveX AI)
2026-07-14 17:40:13 +00:00
parent 6bb355ce17
commit be2aba3919
4 changed files with 350 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
package auth
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"github.com/golang-jwt/jwt/v5"
)
// RS256Service validates RS256 JWT tokens using a public key
// Compatible with ouroboros-identity (port 3208) and aamos-admin-v2
type RS256Service struct {
publicKey *rsa.PublicKey
issuer string
audience string
}
// NewRS256Service loads the public key from a PEM file
func NewRS256Service(publicKeyPath string) (*RS256Service, error) {
pemData, err := os.ReadFile(publicKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read public key: %w", err)
}
block, _ := pem.Decode(pemData)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
// Try PKCS1 format
pub, err = x509.ParsePKCS1PublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse public key: %w", err)
}
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("not an RSA public key")
}
return &RS256Service{
publicKey: rsaPub,
issuer: "prexo-identity",
audience: "prexo",
}, nil
}
// ValidateToken verifies an RS256 JWT token
func (s *RS256Service) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return s.publicKey, nil
})
if err != nil || !token.Valid {
return nil, fmt.Errorf("invalid token: %w", err)
}
mapClaims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid claims format")
}
claims := &Claims{
Sub: getStringClaim(mapClaims, "sub"),
Iss: getStringClaim(mapClaims, "iss"),
Aud: getStringClaim(mapClaims, "aud"),
Exp: getInt64Claim(mapClaims, "exp"),
Iat: getInt64Claim(mapClaims, "iat"),
}
if email, ok := mapClaims["email"].(string); ok {
claims.Email = email
}
if orgID, ok := mapClaims["org_id"].(string); ok {
claims.OrgID = orgID
}
if roles, ok := mapClaims["roles"].([]interface{}); ok {
claims.Roles = make([]string, len(roles))
for i, r := range roles {
claims.Roles[i] = fmt.Sprint(r)
}
}
if scopes, ok := mapClaims["scopes"].([]interface{}); ok {
claims.Scopes = make([]string, len(scopes))
for i, sc := range scopes {
claims.Scopes[i] = fmt.Sprint(sc)
}
}
if err := claims.Valid(); err != nil {
return nil, err
}
return claims, nil
}