Skip to content

Commit

Permalink
feat: update oauthx v.1.2
Browse files Browse the repository at this point in the history
  • Loading branch information
vdbulcke committed Jan 5, 2025
1 parent d99f3e4 commit 6ffd975
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 25 deletions.
7 changes: 5 additions & 2 deletions example/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,11 @@ issuer: "https://idp.iamfas.int.belgium.be/fas/oauth2"
## Http Client Config
##
### Optional
# http_client_config:

# http_client_config:
# ### Add extra headers (map[string]string)
# ### to http request
# extra_headers:
# "x-feature-flag": "jwt-introspection"
# ## MaxIdleConns controls the maximum number of idle (keep-alive)
# ## connections across all hosts. Zero means no limit.
# max_idle_conns: 10
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/hashicorp/go-hclog v1.0.0
github.com/spf13/cobra v1.3.0
github.com/vdbulcke/oauthx v0.1.1
github.com/vdbulcke/oauthx v0.1.2
gopkg.in/yaml.v3 v3.0.1
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/vdbulcke/oauthx v0.1.1 h1:+fEwex0auofY8PLV9rqF18OQgI6UlU4MbIpr1uOQl6A=
github.com/vdbulcke/oauthx v0.1.1/go.mod h1:fObQ0XRBcpkRc0PgcVnulwskHDqKfmvIdyr6uvE8GNI=
github.com/vdbulcke/oauthx v0.1.2 h1:cQowoGN5E7/mTEJuYC7DTt0q08VzkScoNpF3g1xPV4A=
github.com/vdbulcke/oauthx v0.1.2/go.mod h1:I4QjqqzeHnpbfJWTBSsZGrzddjXPFWnrT1ONocniE0U=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down
7 changes: 3 additions & 4 deletions justfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@


scan:
# go list -json -deps | nancy sleuth
trivy fs .
trivy fs . --dependency-tree

build:
goreleaser build --clean
Expand All @@ -12,10 +11,10 @@ build-snapshot:


release-skip-publish:
goreleaser release --clean --skip-publish --skip-sign
goreleaser release --clean --skip=publish,sign

release-snapshot:
goreleaser release --clean --skip-publish --snapshot --skip-sign
goreleaser release --clean --skip=publish,sign --snapshot


lint:
Expand Down
4 changes: 2 additions & 2 deletions src/client/authorize.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func (c *OIDCClient) OIDCAuthorizationCodeFlow() error {
// pushed authorization endpoint and
// only use client_id and request_uri for
// redirect to the authorization_endpoint
oauthx.WithPushedAuthotizationRequest(),
oauthx.WithPushedAuthorizationRequest(),
)

for k, v := range c.config.PARAdditionalParameter {
Expand All @@ -149,7 +149,7 @@ func (c *OIDCClient) OIDCAuthorizationCodeFlow() error {
// generate the 'request' jwt paramater by
// adding authorization options as jwt claims
// oauthx.WithGeneratedRequestJWT(),
oauthx.WithStrictGeneratedRequestJWT(),
oauthx.WithGeneratedRequestJWTOnly(),
)

for k, v := range c.config.JwtRequestAdditionalParameter {
Expand Down
10 changes: 7 additions & 3 deletions src/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ type JSONAccessTokenResponse struct {
}

func (c *OIDCClient) setCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
ttl := 15 * time.Minute
cookie := &http.Cookie{
Name: name,
Value: value,
MaxAge: int(time.Hour.Seconds()),
Name: name,
Value: value,
// MaxAge: int(time.Hour.Seconds()),
MaxAge: int(ttl.Seconds()),
Secure: r.TLS != nil,
HttpOnly: true,
}
Expand All @@ -34,6 +36,8 @@ func (c *OIDCClient) setCallbackCookie(w http.ResponseWriter, r *http.Request, n

func (c *OIDCClient) processAccessTokenResponse(tokenResponse *oauthx.TokenResponse) {

c.logger.Info("AccessToken expiration", "exp", tokenResponse.GetExpiration())

var accessTokenResponse json.RawMessage
err := json.Unmarshal(tokenResponse.Raw, &accessTokenResponse)
if err != nil {
Expand Down
94 changes: 94 additions & 0 deletions src/client/client_credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package oidcclient

import (
"errors"

"github.com/vdbulcke/oauthx"
)

func (c *OIDCClient) ClientCredentialsFlow() error {
scopes := c.config.Scopes
req := oauthx.NewClientCredentialsGrantTokenRequest(scopes...)

tokenResp, err := c.client.DoTokenRequest(c.ctx, req)
if err != nil {
c.logger.Error("Failed to get Access Token", "err", err)

var httpErr *oauthx.HttpErr
if errors.As(err, &httpErr) {
c.logger.Error("http error", "response_headers", httpErr.ResponseHeader, "response_body", string(httpErr.RespBody))
}

return err
}

// Print Access Token
c.processAccessTokenResponse(tokenResp)

if tokenResp.IDToken != "" {

// use default options
idToken, err := c.client.ParseIDToken(c.ctx, tokenResp.IDToken)
if err != nil {
c.logger.Error("ID Token validation failed", "err", err)
return err
}

// print idToken
c.processIdToken(idToken)
}

// Validate Access Token if JWT
// and print claims
if c.config.AccessTokenJwt {
// try to parse access token as JWT
accessTokenRaw := tokenResp.AccessToken
if accessTokenRaw == "" {
c.logger.Error("no Access Token Found")
} else {
// validate signature against the JWK
err := c.processAccessToken(c.ctx, accessTokenRaw)
if err != nil {
c.logger.Error("Access Token validation failed", "err", err)
return err
}

}
}

// Validate refresh Token if JWT
// and print claims
if c.config.RefreshTokenJwt {
// try to parse refresh token as JWT
refreshTokenRaw := tokenResp.RefreshToken
if refreshTokenRaw == "" {
c.logger.Error("no Refresh Token Found")
} else {
// validate signature against the JWK
err := c.processRefreshToken(c.ctx, refreshTokenRaw)
if err != nil {
c.logger.Error("Refresh Token validation failed", "err", err)
return err
}

}
}

// Fetch Userinfo
if !c.config.SkipUserinfo {
userinfo, err := c.client.DoUserinfoRequest(c.ctx, tokenResp.AccessToken)
if err != nil {

var httpErr *oauthx.HttpErr
if errors.As(err, &httpErr) {
c.logger.Error("http error", "response_headers", httpErr.ResponseHeader, "response_body", string(httpErr.RespBody))
}
return err
}

_ = c.userinfo(userinfo)
}

return nil

}
17 changes: 11 additions & 6 deletions src/client/http/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ type HttpClientConfig struct {
Timeout time.Duration `yaml:"timeout_duration" default:"10s"`

InsecureSkipVerify bool `yaml:"skip_tls_verification" default:"false"`

MaxRespSizeLimitBytes int64 `yaml:"limit_max_resp_size_limit" default:"60000"`

ExtraHeader map[string]string `yaml:"extra_headers"`
}

func (c *HttpClientConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
Expand All @@ -61,13 +65,14 @@ func (c *HttpClientConfig) UnmarshalYAML(unmarshal func(interface{}) error) erro

// NewDefaultHttpClientCfg create a default config
func NewDefaultHttpClientCfg() *HttpClientConfig {
return &HttpClientConfig{
MaxIdleConns: 10,
MaxIdleConnsPerHost: 10,
MaxConnsPerHost: 10,
Timeout: 10 * time.Second,
InsecureSkipVerify: false,

cfg := &HttpClientConfig{}
err := defaults.Set(cfg)
if err != nil {
panic(err)
}

return cfg
}

// NewHttpClient Create new http.Transport initialize according
Expand Down
13 changes: 12 additions & 1 deletion src/client/introspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,22 @@ func (c *OIDCClient) IntrospectToken(token string) error {

var httpErr *oauthx.HttpErr
if errors.As(err, &httpErr) {
c.logger.Error("http error", "response_headers", httpErr.ResponseHeader, "response_body", string(httpErr.RespBody))
c.logger.Error("http error", "response_code", httpErr.StatusCode, "response_headers", httpErr.ResponseHeader, "response_body", string(httpErr.RespBody))
rfc6749Err, err := httpErr.AsRFC6749Error()
if err == nil {
c.logger.Error("rfc6749 err", "error", rfc6749Err.Error, "error_description", rfc6749Err.ErrorDescription, "error_uri", rfc6749Err.ErrorUri)
}
}
return err
}

var customClaims struct {
Foo string `json:"foo"`
Bar map[string]interface{}
}

err = resp.UnmarshallClaims(&customClaims)

Check failure on line 38 in src/client/introspect.go

View workflow job for this annotation

GitHub Actions / lint

ineffectual assignment to err (ineffassign)

// snippet only
var result json.RawMessage
err = json.Unmarshal(resp.RawPayload, &result)
Expand Down
30 changes: 26 additions & 4 deletions src/client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package oidcclient
import (
"context"
"crypto/tls"
"errors"
"fmt"
"strings"

Expand Down Expand Up @@ -58,10 +59,18 @@ func NewOIDCClient(c *OIDCClientConfig, privateKey oauthx.OAuthPrivateKey, clien
// http client set custom transport
httpClient := client_http.NewHttpClient(c.HttpClientConfig, l, clientCerts)

limit := c.HttpClientConfig.MaxRespSizeLimitBytes
if limit <= 0 {
limit = oauthx.LIMIT_HTTP_RESP_BODY_MAX_SIZE_BYTES
}

// create a context with trace-id header
ctx := context.Background()
traceId := uuid.New().String()
ctx = tracing.ContextWithTraceID(ctx, "x-trace-id", traceId)
if c.HttpClientConfig != nil && c.HttpClientConfig.ExtraHeader != nil {
ctx = tracing.ContextWithExtraHeader(ctx, c.HttpClientConfig.ExtraHeader)
}
l.Debug("Initial context", "trace_id", traceId)

// Let's starts by getting the AS metadata configuration
Expand All @@ -70,23 +79,36 @@ func NewOIDCClient(c *OIDCClientConfig, privateKey oauthx.OAuthPrivateKey, clien
if c.AlternativeWellKnownEndpoint != "" {
l.Warn("Using Alternative wellknown", "url", c.AlternativeWellKnownEndpoint)

wk, err = oauthx.NewInsecureWellKnownEndpoint(ctx, c.AlternativeWellKnownEndpoint, httpClient)
wk, err = oauthx.NewInsecureWellKnownEndpoint(ctx, c.AlternativeWellKnownEndpoint, oauthx.WellKnownWithHttpClient(httpClient, limit))
if err != nil {

var httpErr *oauthx.HttpErr
if errors.As(err, &httpErr) {
l.Error("http error", "response_headers", httpErr.ResponseHeader, "response_body", string(httpErr.RespBody))
}
return nil, fmt.Errorf("insecure wellknown: %w", err)
}

} else if c.InsecureWellKnownEndpoint {
wkEndpoint := strings.TrimSuffix(c.Issuer, "/") + "/.well-known/openid-configuration"
wk, err = oauthx.NewInsecureWellKnownEndpoint(ctx, wkEndpoint, httpClient)
wk, err = oauthx.NewInsecureWellKnownEndpoint(ctx, wkEndpoint, oauthx.WellKnownWithHttpClient(httpClient, limit))
if err != nil {
var httpErr *oauthx.HttpErr
if errors.As(err, &httpErr) {
l.Error("http error", "response_headers", httpErr.ResponseHeader, "response_body", string(httpErr.RespBody))
}
return nil, fmt.Errorf("oidc wellknown: %w", err)
}

} else {

// fetch /.well-known/openid-configuration
wk, err = oauthx.NewWellKnownOpenidConfiguration(ctx, c.Issuer, httpClient)
wk, err = oauthx.NewWellKnownOpenidConfiguration(ctx, c.Issuer, oauthx.WellKnownWithHttpClient(httpClient, limit))
if err != nil {
var httpErr *oauthx.HttpErr
if errors.As(err, &httpErr) {
l.Error("http error", "response_headers", httpErr.ResponseHeader, "response_body", string(httpErr.RespBody))
}
return nil, fmt.Errorf("oidc wellknown: %w", err)
}
}
Expand Down Expand Up @@ -179,7 +201,7 @@ func NewOIDCClient(c *OIDCClientConfig, privateKey oauthx.OAuthPrivateKey, clien

opts := []oauthx.OAuthClientOptFunc{
oauthx.WithAuthMethod(auth),
oauthx.WithHttpClient(httpClient),
oauthx.WithHttpClientWithLimit(httpClient, limit),
}

if privateKey != nil {
Expand Down
3 changes: 3 additions & 0 deletions src/cmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ func initClient() *oidcclient.OIDCClient {

appLogger := genLogger()

version := buildVersion(Version, GitCommit, Date, BuiltBy)
appLogger.Info("oidc-client", "version", version)

// Parse Config
config, err := oidcclient.ParseConfig(configFilename)
if err != nil {
Expand Down
43 changes: 43 additions & 0 deletions src/cmd/client_credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cmd

import (
"os"

"github.com/spf13/cobra"
)

func init() {
// bind to root command
rootCmd.AddCommand(clientCredentialsCmd)
// add flags to sub command
clientCredentialsCmd.Flags().StringVarP(&configFilename, "config", "c", "", "oidc client config file")
clientCredentialsCmd.Flags().BoolVarP(&skipIdTokenVerification, "skip-id-token-verification", "", false, "Skip validation of id_token after renewing tokens")
clientCredentialsCmd.Flags().StringVarP(&privateKey, "pem-key", "", "", "private key (pem format) for jwt signature or mTLS")
clientCredentialsCmd.Flags().StringVarP(&clientCertificate, "pem-cert", "", "", "client certificate (pem format) mTLS")
clientCredentialsCmd.Flags().StringVarP(&mockKid, "mock-jwt-kid", "", "", "Use static jwt 'kid' value")

// required flags
//nolint
clientCredentialsCmd.MarkFlagRequired("config")

}

var clientCredentialsCmd = &cobra.Command{
Use: "client-credentials",
Short: "Client Credentials Grant Flow",
// Long: "",
Run: func(cmd *cobra.Command, args []string) {
client := initClient()
// set default output
client.SetDefaultOutput()

// display info about the current client
client.Info()

err := client.ClientCredentialsFlow()
if err != nil {
client.GetLogger().Error("Error during Client Credentials grant", "error", err)
os.Exit(1)
}
},
}

0 comments on commit 6ffd975

Please sign in to comment.