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

Make it choosable where client id and secret are put and improve expi… #3970

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -74,17 +74,41 @@ export const OAuth2: Secret = {
type: 'System.String',
uiHint: 'single-line',
},
{
disableWorkflowProviderSelection: false,
isBrowsable: true,
isReadOnly: false,
label: 'Credentials location',
name: 'CredentialsLocation',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: in case you'd want to use the standard term, it's officially called "token endpoint client authentication method".

order: 4,
supportedSyntaxes: ['Literal'],
type: 'System.String',
uiHint: 'dropdown',
options: {
isFlagsEnum: false,
items: [
{
text: 'Authorization header',
value: 'header',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to use the standard values, it's client_secret_basic for that.

},
{
text: 'Form data',
value: 'form',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client_secret_post

}
]
}
},
{
disableWorkflowProviderSelection: false,
isBrowsable: true,
isReadOnly: false,
label: 'Scope',
name: 'Scope',
order: 4,
order: 5,
supportedSyntaxes: ['Literal'],
type: 'System.String',
uiHint: 'single-line',
},
}
],
type: 'OAuth2',
};
Expand Up @@ -43,14 +43,17 @@ public async Task<TokenResponse> GetToken(Secret secret, string? authCode, strin
{
var clientId = secret.GetProperty("ClientId");
var clientSecret = secret.GetProperty("ClientSecret");
var credentialsLocation = secret.GetProperty("CredentialsLocation") ?? "header";
var content = new Dictionary<string, string>
{
{ "grant_type", secret.GetProperty("GrantType") },
{ "client_id", clientId },
{ "client_secret", clientSecret },
{ "offline_access ", "true" }
{ "grant_type", secret.GetProperty("GrantType") }
};

if (credentialsLocation == "form")
{
content.Add("client_id", clientId);
content.Add("client_secret", clientSecret);
}
if (authCode != null)
{
content.Add("code", authCode);
Expand All @@ -67,6 +70,10 @@ public async Task<TokenResponse> GetToken(Secret secret, string? authCode, strin
{
var httpClient = _httpClientFactory.CreateClient(nameof(OAuth2TokenService));
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
if (credentialsLocation == "header")
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Base64Encode(clientId + ":" + clientSecret));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: while many servers implement it this way, your encoding mechanism is not standard: you must formURL-encode the client identifier and secret before base64-encoding them (i.e do Uri.EscapeDataString(value).Replace("%20", "+") on each value).

Depending on the OAuth 2.0 services you want to support, you may want to make that configurable.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, you're not really supposed to use the Authorization header option when you don't have a client secret (in this case, you need to send the client_id alone in the form), but some servers (e.g Reddit) don't implement that correctly and require a quirk.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added escaping, and since it is standard, and seems to be working with google, I wouldn't make it configurable. That secret modal is getting a bit bloated already, so if someone requires it, they can expand upon this feature.
About your second comment, I guess the one who setups the credentials, would just choose the client_secret_post option and not set secret, wouldn't they?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess so. In this case, you'd probably want to add an if to avoid sending an empty client_secret... just in case 😄

Also, Uri.EscapeDataString(clientSecret) will throw if the secret is null (not sure if public clients is a scenario you support).

}

var response = await httpClient.PostAsync(secret.GetProperty("AccessTokenUrl"), new FormUrlEncodedContent(content));
var json = await response.Content.ReadAsStringAsync();
Expand All @@ -92,17 +99,28 @@ public async Task<TokenResponse> GetToken(Secret secret, string? authCode, strin

public async Task<TokenResponse> GetTokenByRefreshToken(Secret secret, string refreshToken)
{
var clientId = secret.GetProperty("ClientId");
var clientSecret = secret.GetProperty("ClientSecret");
var credentialsLocation = secret.GetProperty("CredentialsLocation") ?? "header";
var content = new Dictionary<string, string>
{
{ "grant_type", "refresh_token" },
{ "client_id", secret.GetProperty("ClientId") },
{ "client_secret", secret.GetProperty("ClientSecret") },
{ "refresh_token", refreshToken }
};

if (credentialsLocation == "form")
{
content.Add("client_id", clientId);
content.Add("client_secret", clientSecret);
}

try
{
var httpClient = _httpClientFactory.CreateClient(nameof(OAuth2TokenService));
if (credentialsLocation == "header")
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Base64Encode(clientId + ":" + clientSecret));
}
var response = await httpClient.PostAsync(secret.GetProperty("AccessTokenUrl"), new FormUrlEncodedContent(content));
var json = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<TokenResponse>(json);
Expand Down