Implement KeyCape service-token issuance
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 47s

This commit is contained in:
tegwick 2026-07-27 20:03:07 +02:00
parent 519f0772d2
commit e877d2752d
10 changed files with 348 additions and 43 deletions

View file

@ -5,6 +5,7 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/json"
"net/http"
@ -36,7 +37,7 @@ type tokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
IDToken string `json:"id_token"`
IDToken string `json:"id_token,omitempty"`
}
// ServeHTTP handles POST /token.
@ -49,6 +50,10 @@ func (h *TokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
grantType := r.FormValue("grant_type")
if grantType == "client_credentials" {
h.serveClientCredentials(w, r)
return
}
clientID := r.FormValue("client_id")
code := r.FormValue("code")
codeVerifier := r.FormValue("code_verifier")
@ -183,6 +188,88 @@ func (h *TokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(resp)
}
func (h *TokenHandler) serveClientCredentials(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
clientID, clientSecret, ok := r.BasicAuth()
if !ok {
profileerrors.InvalidProfileUsage("client_secret_basic authentication required", "Authorization").
Write(w, http.StatusUnauthorized)
return
}
client, ok := h.ClientConfig[clientID]
if !ok || client.ClientType != "confidential" || !containsString(client.GrantTypes, "client_credentials") {
profileerrors.InvalidProfileUsage("invalid confidential client", "client_id").
Write(w, http.StatusUnauthorized)
return
}
presentedDigest := sha256.Sum256([]byte(clientSecret))
expectedDigest := sha256.Sum256([]byte(client.ClientSecret))
if client.ClientSecret == "" ||
subtle.ConstantTimeCompare(presentedDigest[:], expectedDigest[:]) != 1 {
profileerrors.InvalidProfileUsage("invalid client authentication", "Authorization").
Write(w, http.StatusUnauthorized)
return
}
requestedScopes := strings.Fields(r.FormValue("scope"))
if len(requestedScopes) == 0 {
requestedScopes = append([]string(nil), client.AllowedScopes...)
}
for _, scope := range requestedScopes {
if !containsString(client.AllowedScopes, scope) {
profileerrors.InvalidProfileUsage("requested scope is not allowed", "scope").
Write(w, http.StatusBadRequest)
return
}
}
now := time.Now()
claims := map[string]interface{}{
"iss": h.Issuer,
"sub": client.ServiceSubject,
"aud": clientID,
"exp": now.Add(h.TokenLifetime).Unix(),
"iat": now.Unix(),
"tenant": client.Tenant,
"principal_type": "service",
"groups": []string{},
"roles": nonNilStrings(client.Roles),
"scope": strings.Join(requestedScopes, " "),
"assurance": map[string]interface{}{
"level": "aal1", "methods": []string{"client_secret"},
"mfa": false, "source": "key-cape", "at": now.Unix(),
},
}
if roles, ok := h.TenantEngine.Roles(ctx, client.Tenant); ok {
claims["tenant_roles"] = roles
}
jwtToken, err := buildJWT(claims, "key-1", h.SigningKey)
if err != nil {
http.Error(w, "failed to build JWT", http.StatusInternalServerError)
return
}
h.Emitter.Emit(ctx, telemetry.Event{
Timestamp: now, EventType: telemetry.EventTokenIssued, ClientID: clientID,
Endpoint: "/token", Result: "success", Scopes: requestedScopes,
GrantType: "client_credentials",
})
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(tokenResponse{
AccessToken: jwtToken, TokenType: "Bearer",
ExpiresIn: int(h.TokenLifetime.Seconds()),
})
}
func containsString(values []string, wanted string) bool {
for _, value := range values {
if value == wanted {
return true
}
}
return false
}
// ---------------------------------------------------------------------------
// IAM Profile core claims (KEY-WP-0005-T01)
// ---------------------------------------------------------------------------