Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support token-exchange after id_token received #61

Merged
merged 5 commits into from
Dec 2, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 69 additions & 9 deletions cmd/openid-client.go
Original file line number Diff line number Diff line change
@@ -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" +
@@ -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" +
@@ -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")
@@ -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 {
@@ -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:
@@ -135,7 +151,7 @@ func main() {
tlsClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
InsecureSkipVerify: *skipTlsVerification,
},
},
}
@@ -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 != "" {
@@ -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 == "" {
@@ -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)
}
}
}

113 changes: 61 additions & 52 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
@@ -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)
@@ -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())
@@ -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)
@@ -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")
@@ -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("==========")

@@ -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 {
@@ -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,
},
},
}
Loading