From 0b7ccb4f8663a904674fc06b7667a4d82f5bfcb1 Mon Sep 17 00:00:00 2001 From: Markus Strehle <11627201+strehle@users.noreply.github.com> Date: Fri, 17 May 2024 15:39:25 +0200 Subject: [PATCH] Support JWT creation from private key + kid only (#36) * Support JWT creation from private key + kid only PKCS12 is nice, but sometimes its easier to use private key in PEM and an own kid to create a JWT for client authentication * Doc update --- README.md | 7 +++++-- client-auth-doc.md | 14 ++++++++++++-- cmd/openid-client.go | 33 +++++++++++++++++++++++++++++++-- pkg/client/client.go | 29 +++++++++++++++++++++++++++-- 4 files changed, 75 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4f94ea9..df176e4 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ make ```text ./openid-client -h Usage: openid-client - This is a CLI to generate tokens from an OIDC complaiant server. Create a service provider/application in the OIDC server with call back url: + This is a CLI to generate tokens from an OpenID Connect (OIDC) complaiant server. Create a service provider/application in the OIDC server with call back url: http://localhost:/callback and set below flags to get an ID token Flags: -issuer IAS. Default is https://.accounts.ondemand.com; XSUAA Default is: https://uaa.cf.eu10.hana.ondemand.com/oauth/token @@ -26,13 +26,16 @@ Flags: -client_secret OIDC client secret. This is an optional flag and only needed for confidential clients. -client_tls P12 file for client mTLS authentication. This is an optional flag and only needed for confidential clients as replacement for client_secret. -client_jwt P12 file for private_key_jwt authentication. This is an optional flag and only needed for confidential clients as replacement for client_secret. + -client_jwt_key Private Key in PEM for private_key_jwt authentication. Use this parameter together with -client_jwt_kid. Replaces -client_jwt and -pin. + -client_jwt_kid Key ID for private_key_jwt authentication. Use this parameter together with -client_jwt_key. Replaces -client_jwt and -pin. -scope OIDC scope parameter. This is an optional flag, default is openid. If you set none, the parameter scope will be omitted in request. -refresh Bool flag. Default false. If true, call refresh flow for the received id_token. -idp_token Bool flag. Default false. If true, call the OIDC IdP token exchange endpoint (IAS specific only) and return the response. -idp_scope OIDC scope parameter. Default no scope is set. If you set the parameter idp_scope, it is set in IdP token exchange endpoint (IAS specific only). -refresh_expiry Value in seconds. Optional parameter to reduce Refresh Token Lifetime. -token_format Format for access_token. Possible values are opaque and jwt. Optional parameter, default: opaque + -cmd Single command to be executed. Supported commands currently: jwks, client_credentials -pin PIN to P12/PKCS12 file using -client_tls or -client_jwt -port Callback port. Open on localhost a port to retrieve the authorization code. Optional parameter, default: 8080 - -h Show this help for more details. + -h Show this help ``` diff --git a/client-auth-doc.md b/client-auth-doc.md index b4f08e1..ed1fd17 100644 --- a/client-auth-doc.md +++ b/client-auth-doc.md @@ -64,8 +64,19 @@ can be used with this tool. ##### Cons * There are not much clients which support this standard, therefore this tool was created for. +### Howto Use the Private Key only for Private Key JWT -### Howto setup Self-Signed Keys for Private Key JWT +1. Optional create key pairs and X509 certificate +```bash +openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -subj "/C=DE/ST=BW/L=Walldorf/O=SAP/OU=Security/CN=localhost" +``` + +2. Use the private key for client authentication +```bash +openid-client -issuer -client_id 11111111-your-client-11111111 -client_jwt_kid ./key.pem -client_jwt_kid key-id-1 +``` + +### Howto setup PKCS12 Key+Certificate for Private Key JWT 1. Optional create key pairs and X509 certificate ```bash @@ -87,6 +98,5 @@ openssl pkcs12 -export -legacy -inkey key.pem -in cert.pem -out final_result.p12 openid-client -issuer -client_id 11111111-your-client-11111111 -client_jwt ./final_result.p12 -pin Test1234 ``` - ### IAS OIDC https://help.sap.com/docs/identity-authentication/identity-authentication/openid-connect \ No newline at end of file diff --git a/cmd/openid-client.go b/cmd/openid-client.go index 119dbd2..194cd6d 100644 --- a/cmd/openid-client.go +++ b/cmd/openid-client.go @@ -7,6 +7,7 @@ import ( "encoding/pem" "flag" "fmt" + "github.com/golang-jwt/jwt/v5" "io/ioutil" "log" "net/http" @@ -30,6 +31,8 @@ func main() { " -client_secret OIDC client secret. This is an optional flag and only needed for confidential clients.\n" + " -client_tls P12 file for client mTLS authentication. This is an optional flag and only needed for confidential clients as replacement for client_secret.\n" + " -client_jwt P12 file for private_key_jwt authentication. This is an optional flag and only needed for confidential clients as replacement for client_secret.\n" + + " -client_jwt_key Private Key in PEM for private_key_jwt authentication. Use this parameter together with -client_jwt_kid. Replaces -client_jwt and -pin.\n" + + " -client_jwt_kid Key ID for private_key_jwt authentication. Use this parameter together with -client_jwt_key. Replaces -client_jwt and -pin.\n" + " -scope OIDC scope parameter. This is an optional flag, default is openid. If you set none, the parameter scope will be omitted in request.\n" + " -refresh Bool flag. Default false. If true, call refresh flow for the received id_token.\n" + " -idp_token Bool flag. Default false. If true, call the OIDC IdP token exchange endpoint (IAS specific only) and return the response.\n" + @@ -39,7 +42,7 @@ func main() { " -cmd Single command to be executed. Supported commands currently: jwks, client_credentials\n" + " -pin PIN to P12/PKCS12 file using -client_tls or -client_jwt \n" + " -port Callback port. Open on localhost a port to retrieve the authorization code. Optional parameter, default: 8080\n" + - " -h Show this help") + " -h Show this help for more details.") } var issEndPoint = flag.String("issuer", "", "OIDC Issuer URI") @@ -55,6 +58,8 @@ func main() { var clientPkcs12 = flag.String("client_tls", "", "PKCS12 file for OIDC client mTLS authentication") var clientJwtPkcs12 = flag.String("client_jwt", "", "PKCS12 file for OIDC private_key_jwt authentication") var pin = flag.String("pin", "", "PIN to PKCS12 file") + var clientJwtKey = flag.String("client_jwt_key", "", "Private Key signing the client JWT for private_key_jwt authentication") + var clientJwtKid = flag.String("client_jwt_kid", "", "Key ID of client JWT for private_key_jwt authentication") var command = flag.String("cmd", "", "Single command to be executed") var mTLS bool = false var privateKeyJwt string = "" @@ -157,6 +162,28 @@ func main() { } mTLS = true } + } else if *clientJwtKey != "" { + if *clientJwtKid == "" { + log.Fatal("client_jwt_kid is required to run this command") + return + } + pemKey, readerror := ioutil.ReadFile(*clientJwtKey) + if readerror != nil { + log.Println("read private key failed") + log.Println(readerror) + return + } + signKey, err := jwt.ParseRSAPrivateKeyFromPEM(pemKey) + if err != nil { + log.Println("decode of RSA private key failed") + log.Println(err) + return + } + privateKeyJwt, err = client.CreatePrivateKeyJwtKid(*clientID, *clientJwtKid, claims.TokenEndPoint, signKey) + if err != nil { + log.Fatal(err) + return + } } requestMap := url.Values{} @@ -168,8 +195,10 @@ func main() { requestMap.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") requestMap.Set("client_assertion", privateKeyJwt) } + var verbose = true if *tokenFormatParameter != "" { requestMap.Set("token_format", *tokenFormatParameter) + verbose = false } if *refreshExpiry != "" { requestMap.Set("refresh_expiry", *refreshExpiry) @@ -177,7 +206,7 @@ func main() { if *command != "" { if *command == "client_credentials" { - client.HandleClientCredential(requestMap, *provider, *tlsClient) + client.HandleClientCredential(requestMap, *provider, *tlsClient, verbose) } else if *command == "jwks" { } } else { diff --git a/pkg/client/client.go b/pkg/client/client.go index 3953190..1bcdfd7 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -304,7 +304,7 @@ func HandleRefreshFlow(clientID string, clientSecret string, existingRefresh str return refreshToken } -func HandleClientCredential(request url.Values, provider oidc.Provider, tlsClient http.Client) string { +func HandleClientCredential(request url.Values, provider oidc.Provider, tlsClient http.Client, verbose bool) string { refreshToken := "" request.Set("grant_type", "client_credentials") req, requestError := http.NewRequest("POST", provider.Endpoint().TokenURL, strings.NewReader(request.Encode())) @@ -329,7 +329,11 @@ func HandleClientCredential(request url.Values, provider oidc.Provider, tlsClien if myToken.AccessToken == "" { fmt.Println(string(jsonStr)) } else { - fmt.Println("Access Token: " + myToken.AccessToken) + if verbose { + fmt.Println("Access Token: " + myToken.AccessToken) + } else { + fmt.Println(myToken.AccessToken) + } } } return refreshToken @@ -359,6 +363,27 @@ func CreatePrivateKeyJwt(clientID string, x509Cert x509.Certificate, tokenEndpoi return tokenString, nil } +func CreatePrivateKeyJwtKid(clientID string, keyId string, tokenEndpoint string, privateKey crypto.PrivateKey) (string, error) { + now := time.Now().UTC() + + claims := make(jwt.MapClaims) + claims["iss"] = clientID // Our clientID + claims["sub"] = clientID // Our clientID + claims["aud"] = tokenEndpoint // The token endpoint of receiver + claims["exp"] = now.Add(time.Minute * 5).Unix() // The expiration time after which the token must be disregarded. + claims["iat"] = now.Unix() // The time at which the token was issued. + claims["jti"] = uuid.New().String() // The jti + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) // .SignedString(key) + token.Header["kid"] = keyId + tokenString, err := token.SignedString(privateKey) + if err != nil { + return "", fmt.Errorf("create: sign token: %w", err) + } + + return tokenString, nil +} + func CalculateSha1ThumbPrint(x509Cert x509.Certificate) string { certSum := sha1.Sum(x509Cert.Raw) return base64.RawURLEncoding.EncodeToString(certSum[:])