package scenarios import ( "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "net" "net/http" "net/url" "strconv" "strings" "time" "github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v3/jwt" "github.com/google/uuid" "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding/jws" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/testenv" "github.com/pomerium/pomerium/internal/testenv/upstreams" "github.com/pomerium/pomerium/internal/testenv/values" "github.com/pomerium/pomerium/pkg/grpc/identity" ) type IDP struct { id values.Value[string] url values.Value[string] publicJWK jose.JSONWebKey signingKey jose.SigningKey stateEncoder encoding.MarshalUnmarshaler userLookup map[string]*User } // Attach implements testenv.Modifier. func (idp *IDP) Attach(ctx context.Context) { env := testenv.EnvFromContext(ctx) router := upstreams.HTTP(nil) idp.url = values.Bind2(env.SubdomainURL("mock-idp"), router.Port(), func(urlStr string, port int) string { u, _ := url.Parse(urlStr) host, _, _ := net.SplitHostPort(u.Host) return u.ResolveReference(&url.URL{ Scheme: "http", Host: fmt.Sprintf("%s:%d", host, port), }).String() }) var err error idp.stateEncoder, err = jws.NewHS256Signer(env.SharedSecret()) env.Require().NoError(err) idp.id = values.Bind2(idp.url, env.AuthenticateURL(), func(idpUrl, authUrl string) string { provider := identity.Provider{ AuthenticateServiceUrl: authUrl, ClientId: "CLIENT_ID", ClientSecret: "CLIENT_SECRET", Type: "oidc", Scopes: []string{"openid", "email", "profile"}, Url: idpUrl, } return provider.Hash() }) router.Handle("/.well-known/jwks.json", func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(&jose.JSONWebKeySet{ Keys: []jose.JSONWebKey{idp.publicJWK}, }) }) router.Handle("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { log.Ctx(ctx).Debug().Str("method", r.Method).Str("uri", r.RequestURI).Send() rootURL, _ := url.Parse(idp.url.Value()) _ = json.NewEncoder(w).Encode(map[string]interface{}{ "issuer": rootURL.String(), "authorization_endpoint": rootURL.ResolveReference(&url.URL{Path: "/oidc/auth"}).String(), "token_endpoint": rootURL.ResolveReference(&url.URL{Path: "/oidc/token"}).String(), "jwks_uri": rootURL.ResolveReference(&url.URL{Path: "/.well-known/jwks.json"}).String(), "userinfo_endpoint": rootURL.ResolveReference(&url.URL{Path: "/oidc/userinfo"}).String(), "id_token_signing_alg_values_supported": []string{ "ES256", }, }) }) router.Handle("/oidc/auth", idp.HandleAuth) router.Handle("/oidc/token", idp.HandleToken) router.Handle("/oidc/userinfo", idp.HandleUserInfo) env.AddUpstream(router) } // Modify implements testenv.Modifier. func (idp *IDP) Modify(cfg *config.Config) { cfg.Options.Provider = "oidc" cfg.Options.ProviderURL = idp.url.Value() cfg.Options.ClientID = "CLIENT_ID" cfg.Options.ClientSecret = "CLIENT_SECRET" cfg.Options.Scopes = []string{"openid", "email", "profile"} } var _ testenv.Modifier = (*IDP)(nil) func NewIDP(users []*User) *IDP { privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { panic(err) } publicKey := &privateKey.PublicKey signingKey := jose.SigningKey{ Algorithm: jose.ES256, Key: privateKey, } publicJWK := jose.JSONWebKey{ Key: publicKey, Algorithm: string(jose.ES256), Use: "sig", } thumbprint, err := publicJWK.Thumbprint(crypto.SHA256) if err != nil { panic(err) } publicJWK.KeyID = hex.EncodeToString(thumbprint) userLookup := map[string]*User{} for _, user := range users { user.ID = uuid.NewString() userLookup[user.ID] = user } return &IDP{ publicJWK: publicJWK, signingKey: signingKey, userLookup: userLookup, } } // HandleAuth handles the auth flow for OIDC. func (idp *IDP) HandleAuth(w http.ResponseWriter, r *http.Request) { rawRedirectURI := r.FormValue("redirect_uri") if rawRedirectURI == "" { http.Error(w, "missing redirect_uri", http.StatusBadRequest) return } redirectURI, err := url.Parse(rawRedirectURI) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } rawClientID := r.FormValue("client_id") if rawClientID == "" { http.Error(w, "missing client_id", http.StatusBadRequest) return } rawEmail := r.FormValue("email") if rawEmail != "" { http.Redirect(w, r, redirectURI.ResolveReference(&url.URL{ RawQuery: (url.Values{ "state": {r.FormValue("state")}, "code": {State{ Email: rawEmail, ClientID: rawClientID, }.Encode()}, }).Encode(), }).String(), http.StatusFound) return } serveHTML(w, `