From 01abe81855610cf78b7f4fa3cf6e85658fd1a01c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 30 Nov 2023 13:27:40 +0100 Subject: [PATCH 1/5] Add basic OIDC implementation to create and validate tokens --- .gitlab-ci.yml | 2 +- oidc/discovery.go | 1 + oidc/keys.go | 41 +++++++++++++++++++++++++++++++++++++++++ oidc/keys_test.go | 17 +++++++++++++++++ oidc/provider.go | 6 ++++++ oidc/token.go | 1 + 6 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 oidc/discovery.go create mode 100644 oidc/keys.go create mode 100644 oidc/keys_test.go create mode 100644 oidc/provider.go create mode 100644 oidc/token.go diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a01ebe9..f8b95db 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 diff --git a/oidc/discovery.go b/oidc/discovery.go new file mode 100644 index 0000000..1df929b --- /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 0000000..c729167 --- /dev/null +++ b/oidc/keys.go @@ -0,0 +1,41 @@ +package oidc + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/base64" + "fmt" +) + +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() + + return base64.URLEncoding.EncodeToString(b) +} + +// RFC https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.1.2 +func (k *Keys) PublicKeyExponent() string { + b := []byte(fmt.Sprintf("%d", k.rsa.PublicKey.E)) + + return base64.URLEncoding.EncodeToString(b) +} + +// RFC https://datatracker.ietf.org/doc/html/rfc7517 +func (k *Keys) DiscoverKeys() { +} diff --git a/oidc/keys_test.go b/oidc/keys_test.go new file mode 100644 index 0000000..6dbf3a0 --- /dev/null +++ b/oidc/keys_test.go @@ -0,0 +1,17 @@ +package oidc + +import ( + "encoding/base64" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPublicKey(t *testing.T) { + keys, err := NewKeyPair() + require.NoError(t, err) + + publicKey := keys.PublicKeyModulus() + _, err = base64.URLEncoding.DecodeString(publicKey) + require.NoError(t, err) +} diff --git a/oidc/provider.go b/oidc/provider.go new file mode 100644 index 0000000..38fb0f0 --- /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 0000000..1df929b --- /dev/null +++ b/oidc/token.go @@ -0,0 +1 @@ +package oidc -- GitLab From 65ae424d1153e4a0e04a1d3db357b507dce2db97 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 30 Nov 2023 14:59:52 +0100 Subject: [PATCH 2/5] Calculate key exponent as little endian 3 bytes to base64 encoded --- oidc/keys.go | 14 +++++++++++--- oidc/keys_test.go | 7 +++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/oidc/keys.go b/oidc/keys.go index c729167..89ae617 100644 --- a/oidc/keys.go +++ b/oidc/keys.go @@ -4,7 +4,6 @@ import ( "crypto/rand" "crypto/rsa" "encoding/base64" - "fmt" ) type Keys struct { @@ -31,9 +30,18 @@ func (k *Keys) PublicKeyModulus() string { // RFC https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.1.2 func (k *Keys) PublicKeyExponent() string { - b := []byte(fmt.Sprintf("%d", k.rsa.PublicKey.E)) + b := k.rsa.PublicKey.E + a := make([]byte, 3) - return base64.URLEncoding.EncodeToString(b) + // 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 diff --git a/oidc/keys_test.go b/oidc/keys_test.go index 6dbf3a0..76c08b8 100644 --- a/oidc/keys_test.go +++ b/oidc/keys_test.go @@ -11,7 +11,10 @@ func TestPublicKey(t *testing.T) { keys, err := NewKeyPair() require.NoError(t, err) - publicKey := keys.PublicKeyModulus() - _, err = base64.URLEncoding.DecodeString(publicKey) + modulus := keys.PublicKeyModulus() + _, err = base64.URLEncoding.DecodeString(modulus) require.NoError(t, err) + + exponent := keys.PublicKeyExponent() + require.Equal(t, "AQAB", exponent) } -- GitLab From 40fc9181a339106e118431b50cfa79a38bca4e40 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 30 Nov 2023 15:07:19 +0100 Subject: [PATCH 3/5] Add test showing that key modulus has the right size --- oidc/keys.go | 6 ++++++ oidc/keys_test.go | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/oidc/keys.go b/oidc/keys.go index 89ae617..7005673 100644 --- a/oidc/keys.go +++ b/oidc/keys.go @@ -25,6 +25,12 @@ func NewKeyPair() (*Keys, error) { 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) } diff --git a/oidc/keys_test.go b/oidc/keys_test.go index 76c08b8..bacf420 100644 --- a/oidc/keys_test.go +++ b/oidc/keys_test.go @@ -12,8 +12,9 @@ func TestPublicKey(t *testing.T) { require.NoError(t, err) modulus := keys.PublicKeyModulus() - _, err = base64.URLEncoding.DecodeString(modulus) + decoded, err := base64.URLEncoding.DecodeString(modulus) require.NoError(t, err) + require.Equal(t, len(decoded), 256) exponent := keys.PublicKeyExponent() require.Equal(t, "AQAB", exponent) -- GitLab From 15f46d4c01e1c4b10e922dfe043bcf59dcee333a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 30 Nov 2023 15:08:11 +0100 Subject: [PATCH 4/5] Do not run build on development branches --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f8b95db..dd1047b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 -- GitLab From cb7ace2b1317700ba0ee3bec723bd0d6ae195946 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 30 Nov 2023 15:28:32 +0100 Subject: [PATCH 5/5] Add JSON Web Key Sets (JWKS) discovery endpoint --- oidc/keys.go | 25 ++++++++++++++++++++++++- oidc/keys_test.go | 19 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/oidc/keys.go b/oidc/keys.go index 7005673..1919ccc 100644 --- a/oidc/keys.go +++ b/oidc/keys.go @@ -4,6 +4,8 @@ import ( "crypto/rand" "crypto/rsa" "encoding/base64" + "encoding/json" + "net/http" ) type Keys struct { @@ -51,5 +53,26 @@ func (k *Keys) PublicKeyExponent() string { } // RFC https://datatracker.ietf.org/doc/html/rfc7517 -func (k *Keys) DiscoverKeys() { +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 index bacf420..70c5849 100644 --- a/oidc/keys_test.go +++ b/oidc/keys_test.go @@ -2,6 +2,9 @@ package oidc import ( "encoding/base64" + "io/ioutil" + "net/http" + "net/http/httptest" "testing" "github.com/stretchr/testify/require" @@ -19,3 +22,19 @@ func TestPublicKey(t *testing.T) { 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`) +} -- GitLab