Skip to content

Commit

Permalink
Add global parameter identity-uri
Browse files Browse the repository at this point in the history
Added a new global parameter to override the identity server uri. This
makes it easier to use the UiPath services with an external identity
token provider. It was possible before to override the identity server
uri by setting the UIPATH_IDENTITY_URI environment variable but not by
providing a global CLI parameter.

Refactored the code so that the CLI parameter, config value and
environment variable parsing is done in the command_builder like all the
other parameters and removed the env variable parsing from the bearer
authenticator.
  • Loading branch information
thschmitt committed May 9, 2024
1 parent 4978601 commit 9ad8512
Show file tree
Hide file tree
Showing 14 changed files with 123 additions and 139 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ You can either pass global arguments as CLI parameters, set an env variable or s
| `--uri` | `UIPATH_URI` | `uri` | `https://cloud.uipath.com` | URL override |
| `--organization` | `UIPATH_ORGANIZATION` | `string` | | Organization name |
| `--tenant` | `UIPATH_TENANT` | `string` | | Tenant name |
| `--identity-uri` | `UIPATH_IDENTITY_URI` | `uri` | `https://cloud.uipath.com/identity_` | URL override for identity calls |
| | `UIPATH_CLIENT_ID` | `string` | | Client Id |
| | `UIPATH_CLIENT_SECRET` | `string` | | Client Secret |
| | `UIPATH_PAT` | `string` | | Personal Access Token |
Expand Down
16 changes: 10 additions & 6 deletions auth/authenticator_context.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package auth

import "net/url"

// AuthenticatorContext provides information required for authenticating requests.
type AuthenticatorContext struct {
Type string `json:"type"`
Config map[string]interface{} `json:"config"`
Debug bool `json:"debug"`
Insecure bool `json:"insecure"`
Request AuthenticatorRequest `json:"request"`
Type string `json:"type"`
Config map[string]interface{} `json:"config"`
IdentityUri url.URL `json:"identityUri"`
Debug bool `json:"debug"`
Insecure bool `json:"insecure"`
Request AuthenticatorRequest `json:"request"`
}

func NewAuthenticatorContext(
authType string,
config map[string]interface{},
identityUri url.URL,
debug bool,
insecure bool,
request AuthenticatorRequest) *AuthenticatorContext {
return &AuthenticatorContext{authType, config, debug, insecure, request}
return &AuthenticatorContext{authType, config, identityUri, debug, insecure, request}
}
26 changes: 2 additions & 24 deletions auth/bearer_authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ package auth

import (
"fmt"
"net/url"
"os"

"github.com/UiPath/uipathcli/cache"
)

const ClientIdEnvVarName = "UIPATH_CLIENT_ID"
const ClientSecretEnvVarName = "UIPATH_CLIENT_SECRET" //nolint // This is not a secret but just the env variable name
const IdentityUriEnvVarName = "UIPATH_IDENTITY_URI"

// The BearerAuthenticator calls the identity token-endpoint to retrieve a JWT bearer token.
// It requires clientId and clientSecret.
Expand All @@ -26,21 +24,9 @@ func (a BearerAuthenticator) Auth(ctx AuthenticatorContext) AuthenticatorResult
if err != nil {
return *AuthenticatorError(fmt.Errorf("Invalid bearer authenticator configuration: %w", err))
}
identityBaseUri := config.IdentityUri
if identityBaseUri == nil {
requestUrl, err := url.Parse(ctx.Request.URL)
if err != nil {
return *AuthenticatorError(fmt.Errorf("Invalid request url '%s': %w", ctx.Request.URL, err))
}
identityBaseUri, err = url.Parse(fmt.Sprintf("%s://%s/identity_", requestUrl.Scheme, requestUrl.Host))
if err != nil {
return *AuthenticatorError(fmt.Errorf("Invalid identity url '%s': %w", ctx.Request.URL, err))
}
}

identityClient := newIdentityClient(a.cache)
tokenRequest := newTokenRequest(
*identityBaseUri,
config.IdentityUri,
config.GrantType,
config.Scopes,
config.ClientId,
Expand Down Expand Up @@ -85,15 +71,7 @@ func (a BearerAuthenticator) getConfig(ctx AuthenticatorContext) (*bearerAuthent
if err != nil {
return nil, err
}
var uri *url.URL
uriString, err := a.parseRequiredString(ctx.Config, "uri", os.Getenv(IdentityUriEnvVarName))
if err == nil {
uri, err = url.Parse(uriString)
if err != nil {
return nil, fmt.Errorf("Error parsing identity uri: %w", err)
}
}
return newBearerAuthenticatorConfig(grantType, scopes, clientId, clientSecret, properties, uri), nil
return newBearerAuthenticatorConfig(grantType, scopes, clientId, clientSecret, properties, ctx.IdentityUri), nil
}

func (a BearerAuthenticator) parseProperties(config map[string]interface{}) (map[string]string, error) {
Expand Down
4 changes: 2 additions & 2 deletions auth/bearer_authenticator_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type bearerAuthenticatorConfig struct {
ClientId string
ClientSecret string
Properties map[string]string
IdentityUri *url.URL
IdentityUri url.URL
}

func newBearerAuthenticatorConfig(
Expand All @@ -17,6 +17,6 @@ func newBearerAuthenticatorConfig(
clientId string,
clientSecret string,
properties map[string]string,
identityUri *url.URL) *bearerAuthenticatorConfig {
identityUri url.URL) *bearerAuthenticatorConfig {
return &bearerAuthenticatorConfig{grantType, scopes, clientId, clientSecret, properties, identityUri}
}
23 changes: 2 additions & 21 deletions auth/oauth_authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,7 @@ func (a OAuthAuthenticator) Auth(ctx AuthenticatorContext) AuthenticatorResult {
if err != nil {
return *AuthenticatorError(fmt.Errorf("Invalid oauth authenticator configuration: %w", err))
}
identityBaseUri := config.IdentityUri
if identityBaseUri == nil {
requestUrl, err := url.Parse(ctx.Request.URL)
if err != nil {
return *AuthenticatorError(fmt.Errorf("Invalid request url '%s': %w", ctx.Request.URL, err))
}
identityBaseUri, err = url.Parse(fmt.Sprintf("%s://%s/identity_", requestUrl.Scheme, requestUrl.Host))
if err != nil {
return *AuthenticatorError(fmt.Errorf("Invalid identity url '%s': %w", ctx.Request.URL, err))
}
}
token, err := a.retrieveToken(*identityBaseUri, *config, ctx.Insecure)
token, err := a.retrieveToken(config.IdentityUri, *config, ctx.Insecure)
if err != nil {
return *AuthenticatorError(fmt.Errorf("Error retrieving access token: %w", err))
}
Expand Down Expand Up @@ -176,15 +165,7 @@ func (a OAuthAuthenticator) getConfig(ctx AuthenticatorContext) (*oauthAuthentic
if err != nil {
return nil, err
}
var uri *url.URL
uriString, err := a.parseRequiredString(ctx.Config, "uri")
if err == nil {
uri, err = url.Parse(uriString)
if err != nil {
return nil, fmt.Errorf("Error parsing identity uri: %w", err)
}
}
return newOAuthAuthenticatorConfig(clientId, *parsedRedirectUri, scopes, uri), nil
return newOAuthAuthenticatorConfig(clientId, *parsedRedirectUri, scopes, ctx.IdentityUri), nil
}

func (a OAuthAuthenticator) parseRequiredString(config map[string]interface{}, name string) (string, error) {
Expand Down
4 changes: 2 additions & 2 deletions auth/oauth_authenticator_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ type oauthAuthenticatorConfig struct {
ClientId string
RedirectUrl url.URL
Scopes string
IdentityUri *url.URL
IdentityUri url.URL
}

func newOAuthAuthenticatorConfig(
clientId string,
redirectUrl url.URL,
scopes string,
identityUri *url.URL) *oauthAuthenticatorConfig {
identityUri url.URL) *oauthAuthenticatorConfig {
return &oauthAuthenticatorConfig{clientId, redirectUrl, scopes, identityUri}
}
97 changes: 23 additions & 74 deletions auth/oauth_authenticator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestOAuthAuthenticatorNotEnabled(t *testing.T) {
"scopes": "OR.Users",
}
request := NewAuthenticatorRequest("http:/localhost", map[string]string{})
context := NewAuthenticatorContext("login", config, false, false, *request)
context := NewAuthenticatorContext("login", config, createIdentityUrl(""), false, false, *request)

authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
result := authenticator.Auth(*context)
Expand All @@ -44,7 +44,7 @@ func TestOAuthAuthenticatorPreservesExistingHeaders(t *testing.T) {
"my-header": "my-value",
}
request := NewAuthenticatorRequest("http:/localhost", headers)
context := NewAuthenticatorContext("login", config, false, false, *request)
context := NewAuthenticatorContext("login", config, createIdentityUrl(""), false, false, *request)

authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
result := authenticator.Auth(*context)
Expand All @@ -56,46 +56,14 @@ func TestOAuthAuthenticatorPreservesExistingHeaders(t *testing.T) {
}
}

func TestOAuthAuthenticatorInvalidRequestUrl(t *testing.T) {
config := map[string]interface{}{
"clientId": "my-client-id",
"redirectUri": "http://localhost:0",
"scopes": "OR.Users",
}
request := NewAuthenticatorRequest("://invalid", map[string]string{})
context := NewAuthenticatorContext("login", config, false, false, *request)

authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
result := authenticator.Auth(*context)
if result.Error != `Invalid request url '://invalid': parse "://invalid": missing protocol scheme` {
t.Errorf("Expected error with invalid request url, but got: %v", result.Error)
}
}

func TestOAuthAuthenticatorInvalidIdentityUrl(t *testing.T) {
config := map[string]interface{}{
"clientId": "my-client-id",
"redirectUri": "http://localhost:0",
"scopes": "OR.Users",
}
request := NewAuthenticatorRequest("INVALID-URL", map[string]string{})
context := NewAuthenticatorContext("login", config, false, false, *request)

authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
result := authenticator.Auth(*context)
if result.Error != `Invalid identity url 'INVALID-URL': parse ":///identity_": missing protocol scheme` {
t.Errorf("Expected error with invalid request url, but got: %v", result.Error)
}
}

func TestOAuthAuthenticatorInvalidConfig(t *testing.T) {
config := map[string]interface{}{
"clientId": 1,
"redirectUri": "http://localhost:0",
"scopes": "OR.Users",
}
request := NewAuthenticatorRequest("http:/localhost", map[string]string{})
context := NewAuthenticatorContext("login", config, false, false, *request)
context := NewAuthenticatorContext("login", config, createIdentityUrl(""), false, false, *request)

authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
result := authenticator.Auth(*context)
Expand All @@ -109,9 +77,9 @@ func TestOAuthFlowIdentityFails(t *testing.T) {
ResponseStatus: 400,
ResponseBody: "Invalid token request",
}
identityUrl := identityServerFake.Start(t)
identityBaseUrl := identityServerFake.Start(t)

context := createAuthContext(identityUrl)
context := createAuthContext(identityBaseUrl)
loginUrl, resultChannel := callAuthenticator(context)
performLogin(loginUrl, t)

Expand All @@ -126,9 +94,9 @@ func TestOAuthFlowSuccessful(t *testing.T) {
ResponseStatus: 200,
ResponseBody: `{"access_token": "my-access-token", "expires_in": 3600, "token_type": "Bearer", "scope": "OR.Users"}`,
}
identityUrl := identityServerFake.Start(t)
identityBaseUrl := identityServerFake.Start(t)

context := createAuthContext(identityUrl)
context := createAuthContext(identityBaseUrl)
loginUrl, resultChannel := callAuthenticator(context)
performLogin(loginUrl, t)

Expand All @@ -142,42 +110,14 @@ func TestOAuthFlowSuccessful(t *testing.T) {
}
}

func TestOAuthFlowWithCustomIdentityUri(t *testing.T) {
identityServerFake := identityServerFake{
ResponseStatus: 200,
ResponseBody: `{"access_token": "my-access-token", "expires_in": 3600, "token_type": "Bearer", "scope": "OR.Users"}`,
}
identityUrl := identityServerFake.Start(t)
config := map[string]interface{}{
"clientId": newClientId(),
"redirectUri": "http://localhost:0",
"scopes": "OR.Users",
"uri": identityUrl.String() + "/identity_",
}
request := NewAuthenticatorRequest("no-url", map[string]string{})
context := NewAuthenticatorContext("login", config, false, false, *request)

loginUrl, resultChannel := callAuthenticator(*context)
performLogin(loginUrl, t)

result := <-resultChannel
if result.Error != "" {
t.Errorf("Expected no error when performing oauth flow, but got: %v", result.Error)
}
authorizationHeader := result.RequestHeader["Authorization"]
if authorizationHeader != "Bearer my-access-token" {
t.Errorf("Expected JWT bearer token in authorization header, but got: %v", authorizationHeader)
}
}

func TestOAuthFlowIsCached(t *testing.T) {
identityServerFake := identityServerFake{
ResponseStatus: 200,
ResponseBody: `{"access_token": "my-access-token", "expires_in": 3600, "token_type": "Bearer", "scope": "OR.Users"}`,
}
identityUrl := identityServerFake.Start(t)
identityBaseUrl := identityServerFake.Start(t)

context := createAuthContext(identityUrl)
context := createAuthContext(identityBaseUrl)
loginUrl, resultChannel := callAuthenticator(context)
performLogin(loginUrl, t)
<-resultChannel
Expand Down Expand Up @@ -217,9 +157,9 @@ func TestShowsSuccessfullyLoggedInPage(t *testing.T) {
ResponseStatus: 200,
ResponseBody: `{"access_token": "my-access-token", "expires_in": 3600, "token_type": "Bearer", "scope": "OR.Users"}`,
}
identityUrl := identityServerFake.Start(t)
identityBaseUrl := identityServerFake.Start(t)

context := createAuthContext(identityUrl)
context := createAuthContext(identityBaseUrl)
loginUrl, _ := callAuthenticator(context)
responseBody := performLogin(loginUrl, t)

Expand Down Expand Up @@ -283,17 +223,26 @@ func callAuthenticator(context AuthenticatorContext) (url.URL, chan Authenticato
return *url, resultChannel
}

func createAuthContext(identityUrl url.URL) AuthenticatorContext {
func createAuthContext(baseUrl url.URL) AuthenticatorContext {
config := map[string]interface{}{
"clientId": newClientId(),
"redirectUri": "http://localhost:0",
"scopes": "OR.Users",
}
request := NewAuthenticatorRequest(fmt.Sprintf("%s://%s", identityUrl.Scheme, identityUrl.Host), map[string]string{})
context := NewAuthenticatorContext("login", config, false, false, *request)
identityUrl := createIdentityUrl(baseUrl.Host)
request := NewAuthenticatorRequest(fmt.Sprintf("%s://%s", baseUrl.Scheme, baseUrl.Host), map[string]string{})
context := NewAuthenticatorContext("login", config, identityUrl, false, false, *request)
return *context
}

func createIdentityUrl(hostName string) url.URL {
if hostName == "" {
hostName = "localhost"
}
identityUrl, _ := url.Parse(fmt.Sprintf("http://%s/identity_", hostName))
return *identityUrl
}

func performLogin(loginUrl url.URL, t *testing.T) string {
redirectUri := loginUrl.Query().Get("redirect_uri")
state := loginUrl.Query().Get("state")
Expand Down
Loading

0 comments on commit 9ad8512

Please sign in to comment.