Skip to content

Commit

Permalink
Support token-exchange after id_token received (#61)
Browse files Browse the repository at this point in the history
* Support token-exchange after id_token received

* refactor

* add token-exchange and jwt-bearer as command

* Add documentation
  • Loading branch information
strehle authored Dec 2, 2024
1 parent fefb54a commit 9c6dc73
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 61 deletions.
78 changes: 69 additions & 9 deletions cmd/openid-client.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ func main() {
" authorization_code Perform authorization code flow.\n" +
" client_credentials Perform client credentials flow.\n" +
" password Perform resource owner flow, also known as password flow.\n" +
" token-exchange Perform OAuth2 Token Exchange (RFC 8693).\n" +
" jwt-bearer Perform OAuth2 JWT Bearer Grant Type.\n" +
" version Show version.\n" +
" help Show this help for more details.\n" +
"\n" +
Expand All @@ -48,6 +50,7 @@ func main() {
" -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, use value or path to X509 certificate.\n" +
" -client_jwt_x5t Header for private_key_jwt X509 authentication. Use this parameter together with -client_jwt_key. Replaces -client_jwt and -pin, use value or path to X509 certificate.\n" +
" -assertion Input token for token exchanges, e.g. jwt-bearer and token-exchange.\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 @@ -58,15 +61,22 @@ func main() {
" -cmd Single command to be executed. Supported commands currently: jwks, client_credentials, password\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" +
" -login_hint Request parameter login_hint passed to the Corporate IdP.\n" +
" -username User name for command password grant required, else optional.\n" +
" -password User password for command password grant required, else optional.\n" +
" -subject_type Token-Exchange subject type. Type of input assertion.\n" +
" -requested_type Token-Exchange requested type.\n" +
" -provider_name Provider name for token-exchange.\n" +
" -k Skip TLS server certificate verification.\n" +
" -v Verbose. Show more details about calls.\n" +
" -h Show this help for more details.")
}

var issEndPoint = flag.String("issuer", "", "OIDC Issuer URI")
var clientID = flag.String("client_id", "", "OIDC client ID")
var clientSecret = flag.String("client_secret", "", "OIDC client secret")
var doRefresh = flag.Bool("refresh", false, "Refresh the received id_token")
var isVerbose = flag.Bool("v", false, "Show more details about calls")
var scopeParameter = flag.String("scope", "", "OIDC scope parameter")
var doCorpIdpTokenExchange = flag.Bool("idp_token", false, "Return OIDC IdP token response")
var refreshExpiry = flag.String("refresh_expiry", "", "Value in seconds to reduce Refresh Token Lifetime")
Expand All @@ -81,9 +91,15 @@ func main() {
var clientJwtX5t = flag.String("client_jwt_x5t", "", "X5T Header in client JWT for private_key_jwt authentication")
var userName = flag.String("username", "", "User name for command password grant required, else optional")
var userPassword = flag.String("password", "", "User password for command password grant required, else optional")
var loginHint = flag.String("login_hint", "", "Parameter login_hint")
var doVersion = flag.Bool("version", false, "Show version")
var appTid = flag.String("app_tid", "", "Application tenant ID")
var command = flag.String("cmd", "", "Single command to be executed")
var assertionToken = flag.String("assertion", "", "Input token for token exchanges")
var subjectType = flag.String("subject_type", "", "Token input type")
var requestedType = flag.String("requested_type", "", "Token-Exchange requested type")
var providerName = flag.String("provider_name", "", "Provider name for token-exchange")
var skipTlsVerification = flag.Bool("k", false, "Skip TLS server certificate verification")
var mTLS bool = false
var privateKeyJwt string = ""
if len(os.Args) > 1 && strings.HasPrefix(os.Args[1], "-") == false {
Expand All @@ -101,7 +117,7 @@ func main() {
case "version":
showVersion()
return
case "client_credentials", "password", "":
case "client_credentials", "password", "token-exchange", "jwt-bearer", "":
case "authorization_code":
*command = "" /* default command */
default:
Expand Down Expand Up @@ -135,7 +151,7 @@ func main() {
tlsClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
InsecureSkipVerify: *skipTlsVerification,
},
},
}
Expand Down Expand Up @@ -255,17 +271,19 @@ func main() {
requestMap.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
requestMap.Set("client_assertion", privateKeyJwt)
}
var verbose = true
var verbose = *isVerbose
if *tokenFormatParameter != "" {
requestMap.Set("token_format", *tokenFormatParameter)
verbose = false
}
if *appTid != "" {
requestMap.Set("app_tid", *appTid)
}
if *refreshExpiry != "" {
requestMap.Set("refresh_expiry", *refreshExpiry)
}
if *loginHint != "" {
requestMap.Set("login_hint", *loginHint)
}

if *command != "" {
if *scopeParameter != "" {
Expand All @@ -286,18 +304,47 @@ func main() {
}
requestMap.Set("password", *userPassword)
client.HandlePasswordGrant(requestMap, *provider, *tlsClient, verbose)
} else if *command == "token-exchange" {
requestMap.Set("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
if *assertionToken == "" {
log.Fatal("assertion parameter not set. Needed to pass it to subject_token for token-exchange")
}
requestMap.Set("subject_token", *assertionToken)
if *subjectType == "" {
log.Fatal("subject_type parameter not set. Supported parameters for token-exchange are, id_token, access_token, refresh_token, jwt")
}
requestMap.Set("subject_token_type", "urn:ietf:params:oauth:token-type:"+*subjectType)
if *requestedType == "" {
log.Fatal("assertion parameter not set. Needed to pass it to subject_token for token-exchange")
}
requestMap.Set("requested_token_type", "urn:ietf:params:oauth:token-type:"+*requestedType)
if *providerName != "" {
requestMap.Set("resource", "urn:sap:identity:application:provider:name:"+*providerName)
}
var exchangedTokenResponse = client.HandleTokenExchangeGrant(requestMap, *provider, *tlsClient, verbose)
fmt.Println(exchangedTokenResponse)
} else if *command == "jwt-bearer" {
requestMap.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
if *assertionToken == "" {
log.Fatal("assertion parameter not set. Needed to pass it for JWT bearer")
}
requestMap.Set("assertion", *assertionToken)
var jwtBearerTokenResponse = client.HandleJwtBearerGrant(requestMap, *provider, *tlsClient, verbose)
fmt.Println(jwtBearerTokenResponse)
} else if *command == "jwks" {
}
} else {
var idToken, refreshToken = client.HandleOpenIDFlow(*clientID, *appTid, *clientSecret, callbackURL, *scopeParameter, *refreshExpiry, *tokenFormatParameter, *portParameter, claims.EndSessionEndpoint, privateKeyJwt, *provider, *tlsClient)
var idToken, refreshToken = client.HandleOpenIDFlow(requestMap, verbose, callbackURL, *scopeParameter, *tokenFormatParameter, *portParameter, claims.EndSessionEndpoint, privateKeyJwt, *provider, *tlsClient)
if *doRefresh {
if refreshToken == "" {
log.Println("No refresh token received.")
return
}
var newRefresh = client.HandleRefreshFlow(*clientID, *appTid, *clientSecret, refreshToken, *refreshExpiry, privateKeyJwt, *provider)
log.Println("Old refresh token: " + refreshToken)
log.Println("New refresh token: " + newRefresh)
var newRefresh = client.HandleRefreshFlow(*clientID, *appTid, *clientSecret, refreshToken, *refreshExpiry, privateKeyJwt, *skipTlsVerification, *provider)
if verbose {
log.Println("Old refresh token: " + refreshToken)
log.Println("New refresh token: " + newRefresh)
}
}
if *doCorpIdpTokenExchange {
if idToken == "" {
Expand All @@ -310,9 +357,22 @@ func main() {
}
var idpTokenResponse = client.HandleCorpIdpExchangeFlow(*clientID, *clientSecret, idToken, *idpScopeParameter, privateKeyJwt, *provider, *tlsClient)
data, _ := json.MarshalIndent(idpTokenResponse, "", " ")
fmt.Println("Response from endpoint /exchange/corporateidp")
if verbose {
fmt.Println("Response from endpoint /exchange/corporateidp")
}
fmt.Println(string(data))
}
if *requestedType != "" && idToken != "" {
requestMap.Set("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
requestMap.Set("subject_token_type", "urn:ietf:params:oauth:token-type:id_token")
requestMap.Set("subject_token", idToken)
requestMap.Set("requested_token_type", "urn:ietf:params:oauth:token-type:"+*requestedType)
if *providerName != "" {
requestMap.Set("resource", "urn:sap:identity:application:provider:name:"+*providerName)
}
var exchangedTokenResponse = client.HandleTokenExchangeGrant(requestMap, *provider, *tlsClient, verbose)
fmt.Println(exchangedTokenResponse)
}
}
}

Expand Down
113 changes: 61 additions & 52 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ func (h *callbackEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.shutdownSignal <- "shutdown"
}

func HandleOpenIDFlow(clientID, appTid, clientSecret, callbackURL string, scopeParameter string, refreshExpiry string, tokenFormatParameter string, port string, endsession string, privateKeyJwt string, provider oidc.Provider, tlsClient http.Client) (string, string) {
func HandleOpenIDFlow(request url.Values, verbose bool, callbackURL string, scopeParameter string, tokenFormatParameter string, port string, endsession string, privateKeyJwt string, provider oidc.Provider, tlsClient http.Client) (string, string) {

refreshToken := ""
idToken := ""
clientID := request.Get("client_id")
authrizationScope := "openid"
callbackEndpoint := &callbackEndpoint{}
callbackEndpoint.shutdownSignal = make(chan string)
Expand Down Expand Up @@ -109,6 +110,9 @@ func HandleOpenIDFlow(clientID, appTid, clientSecret, callbackURL string, scopeP
query.Set("code_challenge_method", "S256")
query.Set("redirect_uri", callbackURL)
query.Set("state", endsession+"?client_id="+clientID)
if request.Has("login_hint") {
query.Set("login_hint", request.Get("login_hint"))
}
authzURL.RawQuery = query.Encode()

//cmd := exec.Command("open", authzURL.String())
Expand Down Expand Up @@ -139,9 +143,10 @@ func HandleOpenIDFlow(clientID, appTid, clientSecret, callbackURL string, scopeP

<-callbackEndpoint.shutdownSignal
callbackEndpoint.server.Shutdown(context.Background())
fmt.Println("")
fmt.Println("Authorization code is ", callbackEndpoint.code)

if verbose {
fmt.Println("")
fmt.Println("Authorization code is ", callbackEndpoint.code)
}
vals := url.Values{}
vals.Set("grant_type", "authorization_code")
vals.Set("code", callbackEndpoint.code)
Expand All @@ -150,14 +155,14 @@ func HandleOpenIDFlow(clientID, appTid, clientSecret, callbackURL string, scopeP
vals.Set("token_format", tokenFormatParameter)
//vals.Set("code_verifier", "01234567890123456789012345678901234567890123456789")
vals.Set("client_id", clientID)
if clientSecret != "" {
vals.Set("client_secret", clientSecret)
if request.Has("client_secret") {
vals.Set("client_secret", request.Get("client_secret"))
}
if appTid != "" {
vals.Set("app_tid", appTid)
if request.Has("app_tid") {
vals.Set("app_tid", request.Get("app_tid"))
}
if refreshExpiry != "" {
vals.Set("refresh_expiry", refreshExpiry)
if request.Has("refresh_expiry") {
vals.Set("refresh_expiry", request.Get("refresh_expiry"))
}
if privateKeyJwt != "" {
vals.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
Expand All @@ -184,7 +189,9 @@ func HandleOpenIDFlow(clientID, appTid, clientSecret, callbackURL string, scopeP
var outBodyMap map[string]interface{}
json.Unmarshal(result, &outBodyMap)
resultJson, _ := json.MarshalIndent(outBodyMap, "", " ")
fmt.Println("OIDC Response Body")
if verbose {
fmt.Println("OIDC Response Body")
}
fmt.Println(string(resultJson))
fmt.Println("==========")

Expand All @@ -210,46 +217,48 @@ func HandleOpenIDFlow(clientID, appTid, clientSecret, callbackURL string, scopeP
// refresh token
stanardToken.RefreshToken = myToken.RefreshToken
refreshToken = myToken.RefreshToken
// Getting now the userInfo
fmt.Println("Call now UserInfo with access_token")
userInfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(&stanardToken))
if err != nil {
log.Fatal(err)
return "", ""
}
oidcConfig := &oidc.Config{
ClientID: clientID,
}
idToken, err := provider.Verifier(oidcConfig).Verify(context.TODO(), myToken.IdToken)
if err != nil {
log.Fatal(err)
return "", ""
}

var outProfile map[string]interface{}
var outUserInfo map[string]interface{}
if err := idToken.Claims(&outProfile); err != nil {
log.Fatal(err)
return "", ""
}
if err := userInfo.Claims(&outUserInfo); err != nil {
log.Fatal(err)
return "", ""
}
data, err := json.MarshalIndent(outProfile, "", " ")
if err != nil {
log.Fatal(err)
return "", ""
}
data2, err := json.MarshalIndent(outUserInfo, "", " ")
if err != nil {
log.Fatal(err)
return "", ""
if verbose {
// Getting now the userInfo
fmt.Println("Call now UserInfo with access_token")
userInfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(&stanardToken))
if err != nil {
log.Fatal(err)
return "", ""
}
oidcConfig := &oidc.Config{
ClientID: clientID,
}
idToken, err := provider.Verifier(oidcConfig).Verify(context.TODO(), myToken.IdToken)
if err != nil {
log.Fatal(err)
return "", ""
}

var outProfile map[string]interface{}
var outUserInfo map[string]interface{}
if err := idToken.Claims(&outProfile); err != nil {
log.Fatal(err)
return "", ""
}
if err := userInfo.Claims(&outUserInfo); err != nil {
log.Fatal(err)
return "", ""
}
data, err := json.MarshalIndent(outProfile, "", " ")
if err != nil {
log.Fatal(err)
return "", ""
}
data2, err := json.MarshalIndent(outUserInfo, "", " ")
if err != nil {
log.Fatal(err)
return "", ""
}
fmt.Println("Claims parsed out from id_token ")
fmt.Println(string(data))
fmt.Println("Claims returned from request to userinfo endpoint ")
fmt.Println(string(data2))
}
fmt.Println("Claims parsed out from id_token ")
fmt.Println(string(data))
fmt.Println("Claims returned from request to userinfo endpoint ")
fmt.Println(string(data2))
}
} else {
if resp.StatusCode != 200 {
Expand All @@ -261,12 +270,12 @@ func HandleOpenIDFlow(clientID, appTid, clientSecret, callbackURL string, scopeP
return idToken, refreshToken
}

func HandleRefreshFlow(clientID string, appTid string, clientSecret string, existingRefresh string, refreshExpiry string, privateKeyJwt string, provider oidc.Provider) string {
func HandleRefreshFlow(clientID string, appTid string, clientSecret string, existingRefresh string, refreshExpiry string, privateKeyJwt string, skipTlsVerification bool, provider oidc.Provider) string {
refreshToken := ""
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
InsecureSkipVerify: skipTlsVerification,
},
},
}
Expand Down
Loading

0 comments on commit 9c6dc73

Please sign in to comment.