Skip to content

Commit

Permalink
Support JWT creation from private key + kid only (#36)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
strehle authored May 17, 2024
1 parent 0442535 commit 0b7ccb4
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 8 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,24 @@ 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:<port>/callback and set below flags to get an ID token
Flags:
-issuer IAS. Default is https://<yourtenant>.accounts.ondemand.com; XSUAA Default is: https://uaa.cf.eu10.hana.ondemand.com/oauth/token
-client_id OIDC client ID. This is a mandatory flag.
-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
```
14 changes: 12 additions & 2 deletions client-auth-doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <yourIAS> -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
Expand All @@ -87,6 +98,5 @@ openssl pkcs12 -export -legacy -inkey key.pem -in cert.pem -out final_result.p12
openid-client -issuer <yourIAS> -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
33 changes: 31 additions & 2 deletions cmd/openid-client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/pem"
"flag"
"fmt"
"github.com/golang-jwt/jwt/v5"
"io/ioutil"
"log"
"net/http"
Expand All @@ -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" +
Expand All @@ -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")
Expand All @@ -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 = ""
Expand Down Expand Up @@ -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{}
Expand All @@ -168,16 +195,18 @@ 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)
}

if *command != "" {
if *command == "client_credentials" {
client.HandleClientCredential(requestMap, *provider, *tlsClient)
client.HandleClientCredential(requestMap, *provider, *tlsClient, verbose)
} else if *command == "jwks" {
}
} else {
Expand Down
29 changes: 27 additions & 2 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand All @@ -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
Expand Down Expand Up @@ -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[:])
Expand Down

0 comments on commit 0b7ccb4

Please sign in to comment.