mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-17 04:37:48 +00:00
... and another all nighter.
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
// Package token provides token management functionality for OIDC authentication.
|
||||
package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Introspector handles token introspection operations
|
||||
type Introspector struct {
|
||||
clientID string
|
||||
clientSecret string
|
||||
introspectionURL string
|
||||
httpClient *http.Client
|
||||
logger LoggerInterface
|
||||
groupsClaimPath []string
|
||||
rolesClaimPath []string
|
||||
extractClaimsRegex string
|
||||
}
|
||||
|
||||
// NewIntrospector creates a new token introspector
|
||||
func NewIntrospector(clientID, clientSecret, introspectionURL string, httpClient *http.Client, logger LoggerInterface, groupsClaimPath, rolesClaimPath []string, extractClaimsRegex string) *Introspector {
|
||||
return &Introspector{
|
||||
clientID: clientID,
|
||||
clientSecret: clientSecret,
|
||||
introspectionURL: introspectionURL,
|
||||
httpClient: httpClient,
|
||||
logger: logger,
|
||||
groupsClaimPath: groupsClaimPath,
|
||||
rolesClaimPath: rolesClaimPath,
|
||||
extractClaimsRegex: extractClaimsRegex,
|
||||
}
|
||||
}
|
||||
|
||||
// IntrospectToken performs token introspection with the OIDC provider
|
||||
func (i *Introspector) IntrospectToken(token string, tokenTypeHint string) (*IntrospectionResponse, error) {
|
||||
if i.introspectionURL == "" {
|
||||
return nil, fmt.Errorf("introspection endpoint not configured")
|
||||
}
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("token", token)
|
||||
if tokenTypeHint != "" {
|
||||
data.Set("token_type_hint", tokenTypeHint)
|
||||
}
|
||||
data.Set("client_id", i.clientID)
|
||||
data.Set("client_secret", i.clientSecret)
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, i.introspectionURL, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create introspection request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := i.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("introspection request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read introspection response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("introspection failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var introspectResp IntrospectionResponse
|
||||
if err := json.Unmarshal(body, &introspectResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse introspection response: %w", err)
|
||||
}
|
||||
|
||||
// Parse any extra fields
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(body, &raw); err == nil {
|
||||
introspectResp.Extra = make(map[string]interface{})
|
||||
for k, v := range raw {
|
||||
switch k {
|
||||
case "active", "scope", "client_id", "username", "token_type",
|
||||
"exp", "iat", "nbf", "sub", "aud", "iss", "jti":
|
||||
// Skip standard fields
|
||||
default:
|
||||
introspectResp.Extra[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &introspectResp, nil
|
||||
}
|
||||
|
||||
// ExtractGroupsAndRoles extracts groups and roles from an ID token
|
||||
func (i *Introspector) ExtractGroupsAndRoles(idToken string) ([]string, []string, error) {
|
||||
jwt, err := parseJWT(idToken)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse ID token: %w", err)
|
||||
}
|
||||
|
||||
groups := i.extractClaimValues(jwt.Claims, i.groupsClaimPath)
|
||||
roles := i.extractClaimValues(jwt.Claims, i.rolesClaimPath)
|
||||
|
||||
i.logger.Logf("Extracted %d groups and %d roles from ID token", len(groups), len(roles))
|
||||
return groups, roles, nil
|
||||
}
|
||||
|
||||
// DetectTokenType analyzes a token and determines its type
|
||||
func (i *Introspector) DetectTokenType(token string) (string, error) {
|
||||
jwt, err := parseJWT(token)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse token: %w", err)
|
||||
}
|
||||
|
||||
// Check for ID token characteristics
|
||||
if aud, ok := jwt.Claims["aud"]; ok {
|
||||
switch v := aud.(type) {
|
||||
case string:
|
||||
if v == i.clientID {
|
||||
return "id_token", nil
|
||||
}
|
||||
case []interface{}:
|
||||
for _, a := range v {
|
||||
if str, ok := a.(string); ok && str == i.clientID {
|
||||
return "id_token", nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for access token characteristics
|
||||
if scope, ok := jwt.Claims["scope"]; ok {
|
||||
if _, isString := scope.(string); isString {
|
||||
return "access_token", nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check token_use claim (AWS Cognito specific)
|
||||
if tokenUse, ok := jwt.Claims["token_use"]; ok {
|
||||
if use, isString := tokenUse.(string); isString {
|
||||
switch use {
|
||||
case "id":
|
||||
return "id_token", nil
|
||||
case "access":
|
||||
return "access_token", nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check typ header
|
||||
if typ, ok := jwt.Header["typ"]; ok {
|
||||
if typStr, isString := typ.(string); isString {
|
||||
switch strings.ToLower(typStr) {
|
||||
case "jwt", "at+jwt":
|
||||
return "access_token", nil
|
||||
case "id+jwt":
|
||||
return "id_token", nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "unknown", nil
|
||||
}
|
||||
|
||||
// extractClaimValues extracts claim values from JWT claims using a path
|
||||
func (i *Introspector) extractClaimValues(claims map[string]interface{}, claimPath []string) []string {
|
||||
if len(claimPath) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result []string
|
||||
current := claims
|
||||
|
||||
for idx, key := range claimPath {
|
||||
if idx == len(claimPath)-1 {
|
||||
// Last key - extract the values
|
||||
if val, exists := current[key]; exists {
|
||||
result = i.extractStringSlice(val)
|
||||
}
|
||||
} else {
|
||||
// Navigate deeper
|
||||
if next, ok := current[key].(map[string]interface{}); ok {
|
||||
current = next
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// extractStringSlice converts various types to string slice
|
||||
func (i *Introspector) extractStringSlice(val interface{}) []string {
|
||||
switch v := val.(type) {
|
||||
case []interface{}:
|
||||
var result []string
|
||||
for _, item := range v {
|
||||
if str, ok := item.(string); ok {
|
||||
result = append(result, str)
|
||||
}
|
||||
}
|
||||
return result
|
||||
case []string:
|
||||
return v
|
||||
case string:
|
||||
if v != "" {
|
||||
// Handle comma-separated or space-separated values
|
||||
if strings.Contains(v, ",") {
|
||||
return strings.Split(v, ",")
|
||||
}
|
||||
return []string{v}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseJWT parses a JWT token without verification
|
||||
func parseJWT(token string) (*JWT, error) {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
|
||||
}
|
||||
|
||||
header, err := decodeSegment(parts[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode header: %w", err)
|
||||
}
|
||||
|
||||
claims, err := decodeSegment(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode claims: %w", err)
|
||||
}
|
||||
|
||||
return &JWT{
|
||||
Header: header,
|
||||
Claims: claims,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// decodeSegment decodes a base64url encoded JWT segment
|
||||
func decodeSegment(seg string) (map[string]interface{}, error) {
|
||||
// Add padding if necessary
|
||||
if l := len(seg) % 4; l > 0 {
|
||||
seg += strings.Repeat("=", 4-l)
|
||||
}
|
||||
|
||||
decoded, err := base64.URLEncoding.DecodeString(seg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode segment: %w", err)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(decoded, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal segment: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user