diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a01ebe9f566bf0510c3970b2ac63a4db1e365fb2..dd1047bcb942f1f73c6799bc7c897647a2388a58 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,7 +15,7 @@ stages: test: image: golang:1.21.4-alpine3.18 stage: test - script: go test -v . + script: go test -v ./... build: stage: build @@ -29,3 +29,5 @@ build: - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA registry.gitlab.com/grzesiek/glgo-service-tmp:$CI_COMMIT_SHORT_SHA # workaround - docker login -u glgo-registry-write -p $CI_REGISTRY_TMP_TOKEN $CI_REGISTRY # workaround - docker push registry.gitlab.com/grzesiek/glgo-service-tmp:$CI_COMMIT_SHORT_SHA # workaround + only: + - main diff --git a/oidc/discovery.go b/oidc/discovery.go new file mode 100644 index 0000000000000000000000000000000000000000..1df929b00df5c577272f38fbe669729da633c84c --- /dev/null +++ b/oidc/discovery.go @@ -0,0 +1 @@ +package oidc diff --git a/oidc/keys.go b/oidc/keys.go new file mode 100644 index 0000000000000000000000000000000000000000..1919cccd330245257806a7684ff062d95364c330 --- /dev/null +++ b/oidc/keys.go @@ -0,0 +1,78 @@ +package oidc + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/base64" + "encoding/json" + "net/http" +) + +type Keys struct { + rsa *rsa.PrivateKey +} + +func NewKeyPair() (*Keys, error) { + // TODO: in production ready code, we will need to store keys in Vault + + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + return &Keys{rsa: key}, nil +} + +// RFC https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.1.1 +func (k *Keys) PublicKeyModulus() string { + b := k.rsa.PublicKey.N.Bytes() + + // RFC: Note that implementers have found that some cryptographic libraries + // prefix an extra zero-valued octet to the modulus representations they + // return, for instance, returning 257 octets for a 2048-bit key, rather than + // 256. Implementations using such libraries will need to take care to omit + // the extra octet from the base64url-encoded representation. + + return base64.URLEncoding.EncodeToString(b) +} + +// RFC https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.1.2 +func (k *Keys) PublicKeyExponent() string { + b := k.rsa.PublicKey.E + a := make([]byte, 3) + + // RFC: For instance, when representing the value 65537, the octet sequence + // to be base64url-encoded MUST consist of the three octets [1, 0, 1]; the + // resulting representation for this value is "AQAB". + + a[0] = byte(b >> 16) + a[1] = byte(b >> 8) + a[2] = byte(b) + + return base64.URLEncoding.EncodeToString(a) +} + +// RFC https://datatracker.ietf.org/doc/html/rfc7517 +func (k *Keys) DiscoverKeys(w http.ResponseWriter, r *http.Request) { + key := map[string]interface{}{ + "kty": "RSA", + "kid": "first-experimental-key", + "e": k.PublicKeyExponent(), + "n": k.PublicKeyModulus(), + "use": "sig", + "alg": "RS256", + } + + payload := map[string]interface{}{ + "keys": []map[string]interface{}{key}, + } + + response, err := json.Marshal(payload) + if err != nil { + http.Error(w, `{"status":"error"}`, http.StatusInternalServerError) + } else { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(response) + } +} diff --git a/oidc/keys_test.go b/oidc/keys_test.go new file mode 100644 index 0000000000000000000000000000000000000000..70c58494ac6c7807b3f92f3c5232b8d79838ed2c --- /dev/null +++ b/oidc/keys_test.go @@ -0,0 +1,40 @@ +package oidc + +import ( + "encoding/base64" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPublicKey(t *testing.T) { + keys, err := NewKeyPair() + require.NoError(t, err) + + modulus := keys.PublicKeyModulus() + decoded, err := base64.URLEncoding.DecodeString(modulus) + require.NoError(t, err) + require.Equal(t, len(decoded), 256) + + exponent := keys.PublicKeyExponent() + require.Equal(t, "AQAB", exponent) +} + +func TestDiscoverKeys(t *testing.T) { + keys, err := NewKeyPair() + require.NoError(t, err) + + r, err := http.NewRequest("GET", "/jwks", nil) + require.NoError(t, err) + w := httptest.NewRecorder() + + keys.DiscoverKeys(w, r) + require.Equal(t, w.Code, 200) + + res, err := ioutil.ReadAll(w.Result().Body) + require.NoError(t, err) + require.Contains(t, string(res), `first-experimental-key`) +} diff --git a/oidc/provider.go b/oidc/provider.go new file mode 100644 index 0000000000000000000000000000000000000000..38fb0f005529d41ec67c50c315e5d098cd10fc77 --- /dev/null +++ b/oidc/provider.go @@ -0,0 +1,6 @@ +package oidc + +type Provider struct { + Issuer string + Keys *Keys +} diff --git a/oidc/token.go b/oidc/token.go new file mode 100644 index 0000000000000000000000000000000000000000..1df929b00df5c577272f38fbe669729da633c84c --- /dev/null +++ b/oidc/token.go @@ -0,0 +1 @@ +package oidc