199 lines
5.1 KiB
Go

package main
import (
"context"
"net/http"
"strings"
)
type authContextKey string
const userClaimsKey authContextKey = "userClaims"
const tenantContextKey authContextKey = "tenantContext"
type accessClaims struct {
Sub string `json:"sub"`
Email string `json:"email"`
PreferredUsername string `json:"preferred_username"`
AuthorizedParty string `json:"azp"`
TenantID string `json:"tenant_id"`
TenantIDAlt string `json:"tenantId"`
OrganizationID string `json:"organization_id"`
OrganizationIDAlt string `json:"organizationId"`
OrganisationID string `json:"organisation_id"`
OrganisationIDAlt string `json:"organisationId"`
OrgID string `json:"org_id"`
OrgIDAlt string `json:"orgId"`
Permissions []string `json:"permissions"`
Scope string `json:"scope"`
Scopes []string `json:"scopes"`
RealmAccess struct {
Roles []string `json:"roles"`
} `json:"realm_access"`
}
func hasOAuthScope(claims *accessClaims, required string) bool {
if claims == nil {
return false
}
for _, scope := range append(strings.Fields(claims.Scope), claims.Scopes...) {
if scope == required {
return true
}
}
return false
}
type tenantContext struct {
ID string
BypassScoping bool
RequestedByOps bool
}
func (a *app) authMiddleware(next http.Handler) http.Handler {
if a.cfg.AuthDisabled {
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if a.oidcVerifier == nil {
writeError(w, http.StatusServiceUnavailable, "oidc verifier not configured")
return
}
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
writeError(w, http.StatusUnauthorized, "missing authorization header")
return
}
tokenString := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
if tokenString == "" {
writeError(w, http.StatusUnauthorized, "invalid bearer token")
return
}
idToken, err := a.oidcVerifier.Verify(r.Context(), tokenString)
if err != nil {
writeError(w, http.StatusUnauthorized, "invalid token")
return
}
claims := &accessClaims{}
if err := idToken.Claims(claims); err != nil {
writeError(w, http.StatusUnauthorized, "invalid token claims")
return
}
if expected := strings.TrimSpace(a.cfg.KeycloakAudience); expected != "" {
if !contains(idToken.Audience, expected) && strings.TrimSpace(claims.AuthorizedParty) != expected {
writeError(w, http.StatusUnauthorized, "token client is not authorized for this application")
return
}
}
tenantID := resolveClaimsTenantID(claims)
isPlatformRole := hasAnyRole(claims, "platform_owner", "platform_admin")
requestedTenantID := strings.TrimSpace(r.Header.Get("X-Tenant-Id"))
if requestedTenantID != "" {
if !isPlatformRole && tenantID != requestedTenantID {
writeError(w, http.StatusForbidden, "tenant override requires platform role")
return
}
tenantID = requestedTenantID
}
ctx := context.WithValue(r.Context(), userClaimsKey, claims)
ctx = context.WithValue(ctx, tenantContextKey, tenantContext{
ID: tenantID,
BypassScoping: isPlatformRole,
RequestedByOps: requestedTenantID != "",
})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func withCORS(allowedOrigins []string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if isAllowedOrigin(origin, allowedOrigins) {
w.Header().Set("Access-Control-Allow-Origin", origin)
} else if contains(allowedOrigins, "*") {
w.Header().Set("Access-Control-Allow-Origin", "*")
}
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func isAllowedOrigin(origin string, allowed []string) bool {
if origin == "" {
return false
}
for _, entry := range allowed {
if entry == "*" || entry == origin {
return true
}
}
return false
}
func contains(values []string, needle string) bool {
for _, value := range values {
if value == needle {
return true
}
}
return false
}
func hasRole(claims *accessClaims, role string) bool {
for _, candidate := range claims.RealmAccess.Roles {
if candidate == role {
return true
}
}
return false
}
func resolveClaimsTenantID(claims *accessClaims) string {
if claims == nil {
return ""
}
for _, candidate := range []string{
claims.TenantID,
claims.TenantIDAlt,
claims.OrganizationID,
claims.OrganizationIDAlt,
claims.OrganisationID,
claims.OrganisationIDAlt,
claims.OrgID,
claims.OrgIDAlt,
} {
if trimmed := strings.TrimSpace(candidate); trimmed != "" {
return trimmed
}
}
return ""
}
func resolveClaimsUserID(claims *accessClaims) string {
if claims == nil {
return ""
}
for _, candidate := range []string{claims.Sub, claims.PreferredUsername, claims.Email} {
if trimmed := strings.TrimSpace(candidate); trimmed != "" {
return trimmed
}
}
return ""
}