Skip to content

Commit 30c37be

Browse files
feat: experimental support for JWT profile authentication using ZITADEL (#182)
* feat: #181 first draft for JWT profile auth * #181: add docs and variable expansion * chore: cleanup
1 parent 786ba0a commit 30c37be

File tree

4 files changed

+64
-3
lines changed

4 files changed

+64
-3
lines changed

src/config.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package src
22

33
import (
44
"context"
5+
"crypto/rsa"
56
"crypto/tls"
67
"crypto/x509"
78
"encoding/base64"
@@ -12,6 +13,8 @@ import (
1213
"strings"
1314
"text/template"
1415

16+
"github.com/golang-jwt/jwt/v5"
17+
1518
"github.com/sevensolutions/traefik-oidc-auth/src/errorPages"
1619
"github.com/sevensolutions/traefik-oidc-auth/src/logging"
1720
"github.com/sevensolutions/traefik-oidc-auth/src/rules"
@@ -70,8 +73,10 @@ type ProviderConfig struct {
7073
CABundle string `json:"ca_bundle"`
7174
CABundleFile string `json:"ca_bundle_file"`
7275

73-
ClientId string `json:"client_id"`
74-
ClientSecret string `json:"client_secret"`
76+
ClientId string `json:"client_id"`
77+
ClientSecret string `json:"client_secret"`
78+
ClientJwtPrivateKey string `json:"client_jwt_private_key"`
79+
ClientJwtPrivateKeyId string `json:"client_jwt_private_key_id"`
7580

7681
UsePkce string `json:"use_pkce"`
7782
UsePkceBool bool `json:"use_pkce_bool"`
@@ -196,6 +201,8 @@ func New(uctx context.Context, next http.Handler, config *Config, name string) (
196201
config.Provider.Url = utils.ExpandEnvironmentVariableString(config.Provider.Url)
197202
config.Provider.ClientId = utils.ExpandEnvironmentVariableString(config.Provider.ClientId)
198203
config.Provider.ClientSecret = utils.ExpandEnvironmentVariableString(config.Provider.ClientSecret)
204+
config.Provider.ClientJwtPrivateKeyId = utils.ExpandEnvironmentVariableString(config.Provider.ClientJwtPrivateKeyId)
205+
config.Provider.ClientJwtPrivateKey = utils.ExpandEnvironmentVariableString(config.Provider.ClientJwtPrivateKey)
199206
config.Provider.UsePkceBool, err = utils.ExpandEnvironmentVariableBoolean(config.Provider.UsePkce, config.Provider.UsePkceBool)
200207
if err != nil {
201208
return nil, err
@@ -215,6 +222,14 @@ func New(uctx context.Context, next http.Handler, config *Config, name string) (
215222
return nil, err
216223
}
217224

225+
var clientAssertionPrivateKey *rsa.PrivateKey
226+
if config.Provider.ClientJwtPrivateKey != "" && config.Provider.ClientJwtPrivateKeyId != "" {
227+
clientAssertionPrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM([]byte(config.Provider.ClientJwtPrivateKey))
228+
if err != nil {
229+
return nil, err
230+
}
231+
}
232+
218233
config.Provider.CABundle = utils.ExpandEnvironmentVariableString(config.Provider.CABundle)
219234
config.Provider.CABundleFile = utils.ExpandEnvironmentVariableString(config.Provider.CABundleFile)
220235
config.Provider.TokenValidation = utils.ExpandEnvironmentVariableString(config.Provider.TokenValidation)
@@ -335,6 +350,7 @@ func New(uctx context.Context, next http.Handler, config *Config, name string) (
335350
next: next,
336351
httpClient: httpClient,
337352
ProviderURL: parsedURL,
353+
ClientJwtPrivateKey: clientAssertionPrivateKey,
338354
CallbackURL: parsedCallbackURL,
339355
Config: config,
340356
SessionStorage: session.CreateCookieSessionStorage(),

src/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package src
22

33
import (
44
"bytes"
5+
"crypto/rsa"
56
"crypto/sha256"
67
"encoding/base64"
78
"fmt"
@@ -27,6 +28,7 @@ type TraefikOidcAuth struct {
2728
next http.Handler
2829
httpClient *http.Client
2930
ProviderURL *url.URL
31+
ClientJwtPrivateKey *rsa.PrivateKey
3032
CallbackURL *url.URL
3133
Config *Config
3234
SessionStorage session.SessionStorage

src/oidc.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net/url"
1212
"path"
1313
"strings"
14+
"time"
1415

1516
"github.com/golang-jwt/jwt/v5"
1617
"github.com/sevensolutions/traefik-oidc-auth/src/logging"
@@ -84,6 +85,16 @@ func exchangeAuthCode(oidcAuth *TraefikOidcAuth, req *http.Request, authCode str
8485
urlValues.Add("client_secret", oidcAuth.Config.Provider.ClientSecret)
8586
}
8687

88+
if oidcAuth.ClientJwtPrivateKey != nil {
89+
clientAssertionToken, err := oidcAuth.getClientAssertionJwtToken()
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
urlValues.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
95+
urlValues.Add("client_assertion", clientAssertionToken)
96+
}
97+
8798
if oidcAuth.Config.Provider.UsePkceBool {
8899
codeVerifierCookie, err := req.Cookie(getCodeVerifierCookieName(oidcAuth.Config))
89100
if err != nil {
@@ -108,7 +119,7 @@ func exchangeAuthCode(oidcAuth *TraefikOidcAuth, req *http.Request, authCode str
108119

109120
if resp.StatusCode != http.StatusOK {
110121
body, _ := io.ReadAll(resp.Body)
111-
oidcAuth.logger.Log(logging.LevelError, "exchangeAuthCode: received bad HTTP response from Provider: %s", string(body))
122+
oidcAuth.logger.Log(logging.LevelError, "exchangeAuthCode: received bad HTTP response from Provider (Status: %d): %s", resp.StatusCode, string(body))
112123
return nil, errors.New("invalid status code")
113124
}
114125

@@ -172,6 +183,16 @@ func (toa *TraefikOidcAuth) introspectToken(token string) (bool, map[string]inte
172183
"token": {token},
173184
}
174185

186+
if toa.ClientJwtPrivateKey != nil {
187+
clientAssertionToken, err := toa.getClientAssertionJwtToken()
188+
if err != nil {
189+
return false, nil, err
190+
}
191+
192+
data.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
193+
data.Add("client_assertion", clientAssertionToken)
194+
}
195+
175196
//log(toa.Config.LogLevel, LogLevelDebug, "Token: %s", token)
176197

177198
endpoint := toa.DiscoveryDocument.IntrospectionEndpoint
@@ -254,3 +275,23 @@ func (toa *TraefikOidcAuth) renewToken(refreshToken string) (*oidc.OidcTokenResp
254275

255276
return tokenResponse, nil
256277
}
278+
279+
func (toa *TraefikOidcAuth) getClientAssertionJwtToken() (string, error) {
280+
claims := jwt.MapClaims{
281+
"iss": toa.Config.Provider.ClientId,
282+
"sub": toa.Config.Provider.ClientId,
283+
"aud": toa.Config.Provider.Url,
284+
"iat": time.Now().Unix(),
285+
"exp": time.Now().Add(5 * time.Minute).Unix(),
286+
}
287+
288+
assertionToken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
289+
assertionToken.Header["kid"] = toa.Config.Provider.ClientJwtPrivateKeyId
290+
291+
clientAssertionJwt, err := assertionToken.SignedString(toa.ClientJwtPrivateKey)
292+
if err != nil {
293+
return "", err
294+
}
295+
296+
return clientAssertionJwt, nil
297+
}

website/docs/getting-started/middleware-configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ But: If you're using YAML-files for configuration you can use [traefik's templat
5858
| `CABundleFile`* | no | `string` | *none* | Specifies the path to an optional CA certificate bundle in case you're using self-signed certificates for the provider. If you're using Docker, make sure the file is mounted into the traefik container. |
5959
| `ClientId`* | yes | `string` | *none* | The client id of the application. |
6060
| `ClientSecret`* | no | `string` | *none* | The client secret of the application. May not be needed for some providers when using PKCE. |
61+
| `ClientJwtPrivateKeyId`* | no | `string` | *none* | Specifies the key id (`keyId` field in the downloaded file) of a [JWT Profile](https://zitadel.com/docs/guides/integrate/token-introspection/private-key-jwt). Only works with ZITADEL. Note: This is a little bit experimental and not well tested yet. |
62+
| `ClientJwtPrivateKey`* | no | `string` | *none* | Specifies the private key (`key` field in the downloaded file) of a [JWT Profile](https://zitadel.com/docs/guides/integrate/token-introspection/private-key-jwt). Only works with ZITADEL. Note: This is a little bit experimental and not well tested yet. |
6163
| `UsePkce`* | no | `bool` | `false`| Enable PKCE. In this case, a client secret may not be needed for some providers. The following algorithms are supported: *RS*, *EC*, *ES*. |
6264
| `ValidateIssuer`* | no | `bool` | `true` | Specifies whether the `iss` claim in the JWT-token should be validated. |
6365
| `ValidIssuer`* | no | `string` | *discovery document* | The issuer which must be present in the JWT-token. By default this will be read from the OIDC discovery document. |

0 commit comments

Comments
 (0)