summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-04-14 15:53:32 -0600
committermo khan <mo@mokhan.ca>2025-04-14 15:53:32 -0600
commitb12eb55fdb603290e3bc62880f6e9dff538571de (patch)
treea9cfde922e251391f0618f9837d7b63a94156664
parentbb577738ac0359f8c8da0902b5c18af789ddf29d (diff)
feat: connect the sessions controller to oidc provider
-rw-r--r--app/controllers/sessions/controller.go13
-rw-r--r--app/controllers/sessions/controller_test.go56
-rw-r--r--go.mod4
-rw-r--r--go.sum20
-rw-r--r--pkg/oidc/id_token.go39
-rw-r--r--pkg/oidc/metadata.go22
-rw-r--r--pkg/oidc/oidc.go31
-rw-r--r--pkg/oidc/oidc_test.go49
8 files changed, 198 insertions, 36 deletions
diff --git a/app/controllers/sessions/controller.go b/app/controllers/sessions/controller.go
index c75e204..1a709de 100644
--- a/app/controllers/sessions/controller.go
+++ b/app/controllers/sessions/controller.go
@@ -3,19 +3,16 @@ package sessions
import (
"net/http"
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/oidc"
"golang.org/x/oauth2"
)
type Controller struct {
- audience string
- cfg *oauth2.Config
+ cfg *oidc.OpenID
}
-func New(cfg *oauth2.Config, audience string) *Controller {
- return &Controller{
- audience: audience,
- cfg: cfg,
- }
+func New(cfg *oidc.OpenID) *Controller {
+ return &Controller{cfg: cfg}
}
func (c *Controller) MountTo(mux *http.ServeMux) {
@@ -25,6 +22,6 @@ func (c *Controller) MountTo(mux *http.ServeMux) {
func (c *Controller) New(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusFound)
- url := c.cfg.AuthCodeURL("csrf-token", oauth2.SetAuthURLParam("audience", c.audience))
+ url := c.cfg.Config.AuthCodeURL("todo-csrf-token", oauth2.SetAuthURLParam("audience", "todo"))
http.Redirect(w, r, url, http.StatusFound)
}
diff --git a/app/controllers/sessions/controller_test.go b/app/controllers/sessions/controller_test.go
index 51536a4..5018e0c 100644
--- a/app/controllers/sessions/controller_test.go
+++ b/app/controllers/sessions/controller_test.go
@@ -2,31 +2,50 @@ package sessions
import (
"net/http"
+ "net/http/httptest"
"net/url"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/xlgmokha/x/pkg/serde"
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/oidc"
"gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/test"
- "golang.org/x/oauth2"
)
func TestSessions(t *testing.T) {
- audience := "https://sparklelab.example.com"
- cfg := &oauth2.Config{
- ClientID: "client_id",
- ClientSecret: "client_secret",
- RedirectURL: audience + "/callback",
- Scopes: []string{"openid"},
- Endpoint: oauth2.Endpoint{
- AuthStyle: oauth2.AuthStyleAutoDetect,
- AuthURL: "https://gitlab.com/oauth/authorize",
- DeviceAuthURL: "https://gitlab.com/oauth/authorize",
- TokenURL: "https://gitlab.com/oauth/token",
- },
+ srv := httptest.NewServer(nil)
+ srv.Config = &http.Server{
+ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, "/.well-known/openid-configuration", r.URL.Path)
+ require.NoError(t, serde.ToJSON(w, &oidc.Metadata{
+ AuthorizationEndpoint: srv.URL + "/oauth/authorize",
+ ClaimsSupported: []string{"aud"},
+ CodeChallengeMethodsSupported: []string{"plain"},
+ DeviceAuthorizationEndpoint: srv.URL + "/device/authorize",
+ IDTokenSigningAlgValuesSupported: []string{"RS256"},
+ Issuer: srv.URL,
+ JWKSURI: srv.URL + "/jwks",
+ MFAChallengeEndpoint: srv.URL + "/mfa",
+ RegistrationEndpoint: srv.URL + "/users/new",
+ RequestURIParameterSupported: false,
+ ResponseModesSupported: []string{"query"},
+ ResponseTypeSupported: []string{"code"},
+ RevocationEndpoint: srv.URL + "/revoke",
+ ScopesSupported: []string{"oidc"},
+ SubjectTypesSupported: []string{"public"},
+ TokenEndpoint: srv.URL + "/token",
+ TokenEndpointAuthMethodsSupported: []string{"client_secret_post"},
+ UserInfoEndpoint: srv.URL + "/users/me",
+ }))
+ }),
}
+ defer srv.Close()
- controller := New(cfg, audience)
+ cfg, err := oidc.New(t.Context(), srv.URL, "client_id", "client_secret", "callback_url")
+ require.NoError(t, err)
+ controller := New(cfg)
mux := http.NewServeMux()
controller.MountTo(mux)
@@ -41,14 +60,13 @@ func TestSessions(t *testing.T) {
require.NotEmpty(t, w.Header().Get("Location"))
redirectURL, err := url.Parse(w.Header().Get("Location"))
require.NoError(t, err)
- assert.Equal(t, "https", redirectURL.Scheme)
- assert.Equal(t, "gitlab.com", redirectURL.Host)
+ assert.Equal(t, strings.TrimPrefix(srv.URL, "http://"), redirectURL.Host)
assert.Equal(t, "/oauth/authorize", redirectURL.Path)
assert.NotEmpty(t, redirectURL.Query().Get("state"))
assert.Equal(t, "client_id", redirectURL.Query().Get("client_id"))
- assert.Equal(t, "openid", redirectURL.Query().Get("scope"))
- assert.Equal(t, audience, redirectURL.Query().Get("audience"))
- assert.Equal(t, cfg.RedirectURL, redirectURL.Query().Get("redirect_uri"))
+ assert.Equal(t, "openid profile email", redirectURL.Query().Get("scope"))
+ assert.Equal(t, "todo", redirectURL.Query().Get("audience"))
+ assert.Equal(t, cfg.Config.RedirectURL, redirectURL.Query().Get("redirect_uri"))
assert.Equal(t, "code", redirectURL.Query().Get("response_type"))
})
})
diff --git a/go.mod b/go.mod
index 5773311..2b129af 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkl
go 1.24.0
require (
+ github.com/coreos/go-oidc/v3 v3.14.1
github.com/oklog/ulid v1.3.1
github.com/stretchr/testify v1.10.0
github.com/testcontainers/testcontainers-go v0.36.0
@@ -26,6 +27,7 @@ require (
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
@@ -62,7 +64,7 @@ require (
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
- golang.org/x/crypto v0.31.0 // indirect
+ golang.org/x/crypto v0.36.0 // indirect
golang.org/x/sys v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
google.golang.org/protobuf v1.36.6 // indirect
diff --git a/go.sum b/go.sum
index 74825c2..7e527e2 100644
--- a/go.sum
+++ b/go.sum
@@ -12,6 +12,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
+github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
+github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
@@ -31,6 +33,8 @@ github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
+github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -134,16 +138,16 @@ go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
-golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
+golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -160,12 +164,12 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
+golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/pkg/oidc/id_token.go b/pkg/oidc/id_token.go
new file mode 100644
index 0000000..5fc1e63
--- /dev/null
+++ b/pkg/oidc/id_token.go
@@ -0,0 +1,39 @@
+package oidc
+
+import (
+ "bytes"
+ "encoding/base64"
+ "errors"
+ "strings"
+ "time"
+
+ "github.com/xlgmokha/x/pkg/serde"
+)
+
+type IDToken struct {
+ Audience string `json:"aud"`
+ Email string `json:"email"`
+ EmailVerified bool `json:"email_verified"`
+ ExpiredAt int64 `json:"exp"`
+ IssuedAt int64 `json:"iat"`
+ Issuer string `json:"iss"`
+ Name string `json:"name"`
+ Nickname string `json:"nickname"`
+ Picture string `json:"picture"`
+ Subject string `json:"sub"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+func NewIDToken(raw string) (*IDToken, error) {
+ sections := strings.SplitN(raw, ".", 3)
+ if len(sections) != 3 {
+ return nil, errors.New("Invalid token")
+ }
+ b, err := base64.RawURLEncoding.DecodeString(sections[1])
+ if err != nil {
+ return nil, err
+ }
+
+ token, err := serde.FromJSON[*IDToken](bytes.NewReader(b))
+ return token, err
+}
diff --git a/pkg/oidc/metadata.go b/pkg/oidc/metadata.go
new file mode 100644
index 0000000..8678f3b
--- /dev/null
+++ b/pkg/oidc/metadata.go
@@ -0,0 +1,22 @@
+package oidc
+
+type Metadata struct {
+ Issuer string `json:"issuer"`
+ AuthorizationEndpoint string `json:"authorization_endpoint"`
+ TokenEndpoint string `json:"token_endpoint"`
+ DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"`
+ UserInfoEndpoint string `json:"userinfo_endpoint"`
+ MFAChallengeEndpoint string `json:"mfa_challenge_endpoint"`
+ JWKSURI string `json:"jwks_uri"`
+ RegistrationEndpoint string `json:"registration_endpoint"`
+ RevocationEndpoint string `json:"revocation_endpoint"`
+ ScopesSupported []string `json:"scopes_supported"`
+ ResponseTypeSupported []string `json:"response_types_supported"`
+ CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
+ ResponseModesSupported []string `json:"response_modes_supported"`
+ SubjectTypesSupported []string `json:"subject_types_supported"`
+ IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
+ TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
+ ClaimsSupported []string `json:"claims_supported"`
+ RequestURIParameterSupported bool `json:"request_uri_parameter_supported"`
+}
diff --git a/pkg/oidc/oidc.go b/pkg/oidc/oidc.go
new file mode 100644
index 0000000..0526142
--- /dev/null
+++ b/pkg/oidc/oidc.go
@@ -0,0 +1,31 @@
+package oidc
+
+import (
+ "context"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+ "golang.org/x/oauth2"
+)
+
+type OpenID struct {
+ Provider *oidc.Provider
+ Config *oauth2.Config
+}
+
+func New(ctx context.Context, issuer string, clientID, clientSecret, callbackURL string) (*OpenID, error) {
+ provider, err := oidc.NewProvider(ctx, issuer)
+ if err != nil {
+ return nil, err
+ }
+
+ return &OpenID{
+ Provider: provider,
+ Config: &oauth2.Config{
+ ClientID: clientID,
+ ClientSecret: clientSecret,
+ RedirectURL: callbackURL,
+ Endpoint: provider.Endpoint(),
+ Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
+ },
+ }, nil
+}
diff --git a/pkg/oidc/oidc_test.go b/pkg/oidc/oidc_test.go
new file mode 100644
index 0000000..ef6a4f6
--- /dev/null
+++ b/pkg/oidc/oidc_test.go
@@ -0,0 +1,49 @@
+package oidc
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/xlgmokha/x/pkg/serde"
+)
+
+func TestOpenID(t *testing.T) {
+ t.Run("GET /.well-known/openid-configuration", func(t *testing.T) {
+ srv := httptest.NewServer(nil)
+ srv.Config = &http.Server{
+ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, "/.well-known/openid-configuration", r.URL.Path)
+ require.NoError(t, serde.ToJSON(w, &Metadata{
+ AuthorizationEndpoint: srv.URL + "/authorize",
+ ClaimsSupported: []string{"aud"},
+ CodeChallengeMethodsSupported: []string{"plain"},
+ DeviceAuthorizationEndpoint: srv.URL + "/device/authorize",
+ IDTokenSigningAlgValuesSupported: []string{"RS256"},
+ Issuer: srv.URL,
+ JWKSURI: srv.URL + "/jwks",
+ MFAChallengeEndpoint: srv.URL + "/mfa",
+ RegistrationEndpoint: srv.URL + "/users/new",
+ RequestURIParameterSupported: false,
+ ResponseModesSupported: []string{"query"},
+ ResponseTypeSupported: []string{"code"},
+ RevocationEndpoint: srv.URL + "/revoke",
+ ScopesSupported: []string{"oidc"},
+ SubjectTypesSupported: []string{"public"},
+ TokenEndpoint: srv.URL + "/token",
+ TokenEndpointAuthMethodsSupported: []string{"client_secret_post"},
+ UserInfoEndpoint: srv.URL + "/users/me",
+ }))
+ }),
+ }
+ defer srv.Close()
+
+ openID, err := New(context.Background(), srv.URL, "client_id", "client_secret", "https://example.com/oauth/callback")
+ require.NoError(t, err)
+
+ assert.Equal(t, srv.URL+"/authorize", openID.Provider.Endpoint().AuthURL)
+ })
+}