Public source validation / validate (push) Failing after 3m8s
160 lines
4.7 KiB
Go
160 lines
4.7 KiB
Go
package authapi
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"math/big"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// signingKey is generated once per test binary; RSA generation is expensive.
|
|
var signingKey = sync.OnceValue(func() *rsa.PrivateKey {
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return key
|
|
})
|
|
|
|
// fakeIdP is a minimal OIDC provider: discovery document, JWKS and token endpoint.
|
|
// Tests drive its behaviour through the exported fields before calling the callback.
|
|
type fakeIdP struct {
|
|
server *httptest.Server
|
|
key *rsa.PrivateKey
|
|
clientID string
|
|
|
|
mu sync.Mutex
|
|
expectedChallenge string
|
|
nonce string
|
|
subject string
|
|
groups []string
|
|
tokenFails bool
|
|
omitIDToken bool
|
|
issuerOverride string
|
|
verifierSeen string
|
|
}
|
|
|
|
func newFakeIdP(t *testing.T, clientID string) *fakeIdP {
|
|
t.Helper()
|
|
idp := &fakeIdP{key: signingKey(), clientID: clientID, subject: "user-1", groups: []string{"pulse-operator"}}
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/.well-known/openid-configuration", idp.discovery)
|
|
mux.HandleFunc("/jwks", idp.jwks)
|
|
mux.HandleFunc("/token", idp.token)
|
|
mux.HandleFunc("/authorize", func(response http.ResponseWriter, _ *http.Request) {
|
|
response.WriteHeader(http.StatusOK)
|
|
})
|
|
idp.server = httptest.NewServer(mux)
|
|
t.Cleanup(idp.server.Close)
|
|
return idp
|
|
}
|
|
|
|
func (idp *fakeIdP) configure(mutate func(*fakeIdP)) {
|
|
idp.mu.Lock()
|
|
defer idp.mu.Unlock()
|
|
mutate(idp)
|
|
}
|
|
|
|
func (idp *fakeIdP) codeVerifier() string {
|
|
idp.mu.Lock()
|
|
defer idp.mu.Unlock()
|
|
return idp.verifierSeen
|
|
}
|
|
|
|
func (idp *fakeIdP) discovery(response http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(response, http.StatusOK, map[string]any{
|
|
"issuer": idp.server.URL,
|
|
"authorization_endpoint": idp.server.URL + "/authorize",
|
|
"token_endpoint": idp.server.URL + "/token",
|
|
"jwks_uri": idp.server.URL + "/jwks",
|
|
"response_types_supported": []string{"code"},
|
|
"subject_types_supported": []string{"public"},
|
|
"id_token_signing_alg_values_supported": []string{"RS256"},
|
|
})
|
|
}
|
|
|
|
func (idp *fakeIdP) jwks(response http.ResponseWriter, _ *http.Request) {
|
|
public := &idp.key.PublicKey
|
|
writeJSON(response, http.StatusOK, map[string]any{"keys": []map[string]any{{
|
|
"kty": "RSA",
|
|
"kid": "test-key",
|
|
"alg": "RS256",
|
|
"use": "sig",
|
|
"n": base64.RawURLEncoding.EncodeToString(public.N.Bytes()),
|
|
"e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(public.E)).Bytes()),
|
|
}}})
|
|
}
|
|
|
|
func (idp *fakeIdP) token(response http.ResponseWriter, request *http.Request) {
|
|
if err := request.ParseForm(); err != nil {
|
|
writeJSON(response, http.StatusBadRequest, map[string]any{"error": "invalid_request"})
|
|
return
|
|
}
|
|
idp.mu.Lock()
|
|
defer idp.mu.Unlock()
|
|
idp.verifierSeen = request.PostForm.Get("code_verifier")
|
|
if idp.tokenFails {
|
|
writeJSON(response, http.StatusBadRequest, map[string]any{"error": "invalid_grant"})
|
|
return
|
|
}
|
|
if idp.expectedChallenge != "" {
|
|
digest := sha256.Sum256([]byte(idp.verifierSeen))
|
|
if base64.RawURLEncoding.EncodeToString(digest[:]) != idp.expectedChallenge {
|
|
writeJSON(response, http.StatusBadRequest, map[string]any{"error": "invalid_grant"})
|
|
return
|
|
}
|
|
}
|
|
body := map[string]any{"access_token": "opaque-access-token", "token_type": "Bearer", "expires_in": 3600}
|
|
if !idp.omitIDToken {
|
|
issuer := idp.server.URL
|
|
if idp.issuerOverride != "" {
|
|
issuer = idp.issuerOverride
|
|
}
|
|
now := time.Now()
|
|
body["id_token"] = idp.sign(map[string]any{
|
|
"iss": issuer,
|
|
"aud": idp.clientID,
|
|
"sub": idp.subject,
|
|
"iat": now.Unix(),
|
|
"exp": now.Add(5 * time.Minute).Unix(),
|
|
"nonce": idp.nonce,
|
|
"groups": idp.groups,
|
|
})
|
|
}
|
|
writeJSON(response, http.StatusOK, body)
|
|
}
|
|
|
|
func (idp *fakeIdP) sign(claims map[string]any) string {
|
|
segments := []string{encodeSegment(map[string]any{"alg": "RS256", "typ": "JWT", "kid": "test-key"}), encodeSegment(claims)}
|
|
input := strings.Join(segments, ".")
|
|
digest := sha256.Sum256([]byte(input))
|
|
signature, err := rsa.SignPKCS1v15(rand.Reader, idp.key, crypto.SHA256, digest[:])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return input + "." + base64.RawURLEncoding.EncodeToString(signature)
|
|
}
|
|
|
|
func encodeSegment(value map[string]any) string {
|
|
encoded, err := json.Marshal(value)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return base64.RawURLEncoding.EncodeToString(encoded)
|
|
}
|
|
|
|
func writeJSON(response http.ResponseWriter, status int, body any) {
|
|
response.Header().Set("Content-Type", "application/json")
|
|
response.WriteHeader(status)
|
|
_ = json.NewEncoder(response).Encode(body)
|
|
}
|