Skip to content

Commit

Permalink
feat: [#12] Store Okta mock that has been created by wimspaargaren
Browse files Browse the repository at this point in the history
  • Loading branch information
sbp-bvanb committed Dec 18, 2024
1 parent b2bb646 commit a1db927
Show file tree
Hide file tree
Showing 9 changed files with 698 additions and 3 deletions.
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ curl \

## MCVS-Stub-Server

A simple HTTP server which can configure endpoints with a given response. This can be used as a stub server to mimick behaviour of other services.
A simple HTTP server which can configure endpoints with a given response. This
can be used as a stub server to mimic behavior of other services.

### Build

Expand All @@ -64,6 +65,7 @@ docker run -p 8080:8080 stub-server
### Test

**Configuring**

```
curl --location 'localhost:8080/configure' \
--header 'Content-Type: application/json' \
Expand All @@ -74,6 +76,29 @@ curl --location 'localhost:8080/configure' \
```

**Hit a configured endpoint**

```
curl --location 'localhost:8080/foo'
```

## Okta

Generate a valid Okta JSON Web Token (JWT).

### Build

```zsh
docker build -t oktamock --build-arg APPLICATION=oktamock .
```

### Run

```zsh
docker run -p 8080:8080 oktamock
```

### Test

```zsh
curl http://localhost:8080/token
```
7 changes: 5 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ vars:
REMOTE_URL_REPO: schubergphilis/mcvs-golang-action

includes:
remote: >-
{{.REMOTE_URL}}/{{.REMOTE_URL_REPO}}/{{.REMOTE_URL_REF}}/Taskfile.yml
remote:
taskfile: >-
{{.REMOTE_URL}}/{{.REMOTE_URL_REPO}}/{{.REMOTE_URL_REF}}/Taskfile.yml
vars:
GCI_SECTIONS: '-s standard -s default -s "Prefix(schubergphilis/mcvs-integrationtest-services)"'

Check failure on line 14 in Taskfile.yml

View workflow job for this annotation

GitHub Actions / mcvs-docker-action (mcvs-integrationtest-services)

14:81 [line-length] line too long (102 > 80 characters)
253 changes: 253 additions & 0 deletions cmd/oktamock/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
// Package main provides a mocked Okta server which can be used to create and validate JWT tokens.
package main

import (
"crypto/rand"
"crypto/rsa"
"encoding/json"
"fmt"
"log"
"net/http"
"time"

"github.com/caarlos0/env/v9"
"github.com/golang-jwt/jwt/v4"
"github.com/lestrrat-go/jwx/v2/jwk"

"schubergphilis/mcvs-integrationtest-services/internal/oktamock/models"
)

// ErrUnsupportedSigningMethod represents an error when an unsupported signing method is provided.
type ErrUnsupportedSigningMethod struct {
ProvidedMethod string
}

func (e ErrUnsupportedSigningMethod) Error() string {
return fmt.Sprintf("unsupported signing method: %s", e.ProvidedMethod)
}

// SigningMethod represents the signing method for a JWT.
type SigningMethod struct {
actualMethod *jwt.SigningMethodRSA
}

// Alg returns the algorithm as string.
func (s SigningMethod) Alg() string {
return s.actualMethod.Alg()
}

// UnmarshalText marshals the signing method to text.
func (s *SigningMethod) UnmarshalText(text []byte) error {
switch string(text) {
case "RS256":
s.actualMethod = jwt.SigningMethodRS256
return nil
case "RS384":
s.actualMethod = jwt.SigningMethodRS384
return nil
case "RS512":
s.actualMethod = jwt.SigningMethodRS512
return nil
}
return ErrUnsupportedSigningMethod{
ProvidedMethod: string(text),
}
}

// Config represents the configuration.
type Config struct {
ServerConfig ServerConfig
JWTConfig JWTConfig
}

// ServerConfig represents the server configuration.
type ServerConfig struct {
Port int `env:"PORT" envDefault:"8080"`
}

// JWTConfig represents the JWT configuration.
type JWTConfig struct {
Aud string `env:"AUD" envDefault:"api://default"`
Expiration time.Duration `env:"EXPIRATION" envDefault:"24h"`
Groups []string `env:"GROUPS" envDefault:""`
Issuer string `env:"ISSUER" envDefault:"http://localhost:8080"`
KID string `env:"KID" envDefault:"mock-kid"`
SigningMethod SigningMethod `env:"SIGNING_METHOD" envDefault:"RS256"`
}

// NewConfig returns the config.
func NewConfig() (*Config, error) {
cfg := Config{}
err := env.Parse(&cfg)
if err != nil {
return nil, err
}
return &cfg, nil
}

func main() {
cfg, err := NewConfig()
if err != nil {
log.Fatal(err)
}
oktaMockServer, err := NewOktaMockServer(cfg)
if err != nil {
log.Fatal(err)
}

http.HandleFunc("/.well-known/openid-configuration", oktaMockServer.handleOpenIDConfig)
http.HandleFunc("/v1/keys", oktaMockServer.handleGetJWKS)
http.HandleFunc("/token", oktaMockServer.handleGetValidJWT)

//nolint: gosec
err = http.ListenAndServe(fmt.Sprintf(":%d", cfg.ServerConfig.Port), nil)
if err != nil {
log.Fatal(err)
}
}

// OktaMockServer represents a mock Okta server which can be used to create and validate JWT tokens.
// Serves as a subtitute for using an actual Okta Server.
type OktaMockServer struct {
audience, issuer string
expiration time.Duration
groups []string

privKey *rsa.PrivateKey
jwkKey jwk.Key
}

// CustomClaimsRequest represents the JSON structure for requests that include custom claims for JWT tokens.
type CustomClaimsRequest struct {
CustomClaims map[string]interface{} `json:"custom_claims"`
}

func (o *OktaMockServer) handleGetValidJWT(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var claimsReq CustomClaimsRequest
if err := decoder.Decode(&claimsReq); err != nil {
http.Error(w, "Okta mock expects custom claims to be present in token request", http.StatusBadRequest)
return
}

now := time.Now()
claims := jwt.MapClaims{
"aud": o.audience,
"iss": o.issuer,
"iat": now.Unix(),
"exp": now.Add(o.expiration).Unix(),
"nbf": now.AddDate(0, 0, -1).Unix(),
"Groups": o.groups,
}

// Add custom claims
for key, value := range claimsReq.CustomClaims {
claims[key] = value
}

// Create a new token with these claims
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header["kid"] = o.jwkKey.KeyID()

// Generate the signed JWT string.
res, err := token.SignedString(o.privKey)
if err != nil {
log.Default().Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Default().Println("Generated JWT:", res)

// Prepare and send the response.
tokenResponse := models.ValidJWTResponse{
AccessToken: res,
}
b, err := json.Marshal(tokenResponse)
if err != nil {
log.Default().Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = w.Write(b)
if err != nil {
log.Default().Println(err)
}
}

func (o *OktaMockServer) handleGetJWKS(w http.ResponseWriter, _ *http.Request) {
resp := models.JWKSResponse{
Keys: []jwk.Key{o.jwkKey},
}
b, err := json.Marshal(resp)
if err != nil {
log.Default().Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = w.Write(b)
if err != nil {
log.Default().Println(err)
}
}

func (o *OktaMockServer) handleOpenIDConfig(w http.ResponseWriter, _ *http.Request) {
resp := models.OpenIDConfigurationResponse{
JwksURI: fmt.Sprintf("%s/v1/keys", o.issuer),
}
b, err := json.Marshal(resp)
if err != nil {
log.Default().Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = w.Write(b)
if err != nil {
log.Default().Println(err)
}
}

// NewOktaMockServer returns a new OktaMockServer.
func NewOktaMockServer(cfg *Config) (*OktaMockServer, error) {
privKeyRSA, jwkKey, err := genRSAKeyAndJWK(&cfg.JWTConfig)
if err != nil {
return nil, err
}

return &OktaMockServer{
audience: cfg.JWTConfig.Aud,
expiration: cfg.JWTConfig.Expiration,
groups: cfg.JWTConfig.Groups,
issuer: cfg.JWTConfig.Issuer,
jwkKey: jwkKey,
privKey: privKeyRSA,
}, nil
}

func genRSAKeyAndJWK(cfg *JWTConfig) (*rsa.PrivateKey, jwk.Key, error) {
bitSize := 4096

privateKey, err := rsa.GenerateKey(rand.Reader, bitSize)
if err != nil {
return nil, nil, err
}

err = privateKey.Validate()
if err != nil {
return nil, nil, err
}

jwkKey, err := jwk.PublicKeyOf(privateKey.PublicKey)
if err != nil {
return nil, nil, err
}

err = jwkKey.Set(jwk.KeyIDKey, cfg.KID)
if err != nil {
return nil, nil, err
}
err = jwkKey.Set(jwk.AlgorithmKey, cfg.SigningMethod.Alg())
if err != nil {
return nil, nil, err
}
return privateKey, jwkKey, nil
}
36 changes: 36 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,57 @@ module schubergphilis/mcvs-integrationtest-services
go 1.23.4

require (
github.com/caarlos0/env/v9 v9.0.0
github.com/golang-jwt/jwt/v4 v4.5.1
github.com/labstack/echo/v4 v4.13.2
github.com/lestrrat-go/jwx/v2 v2.1.3
github.com/ory/dockertest/v3 v3.11.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.10.0
)

require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/containerd/continuity v0.4.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/docker/cli v26.1.4+incompatible // indirect
github.com/docker/docker v27.1.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.6 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opencontainers/runc v1.1.13 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.32.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit a1db927

Please sign in to comment.