Skip to content

Commit a203e06

Browse files
alex-zylAlex Zyl
authored and
Alex Zyl
committed
openfga#238 Support custom token issuer endpoints
1 parent 29acb29 commit a203e06

File tree

3 files changed

+87
-24
lines changed

3 files changed

+87
-24
lines changed

config/clients/dotnet/template/Client_OAuth2Client.mustache

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public class OAuth2Client {
6464
private AuthToken _authToken = new();
6565
private IDictionary<string, string> _authRequest { get; }
6666
private string _apiTokenIssuer { get; }
67+
private string _apiTokenPath { get; }
6768
private readonly RetryParams _retryParams;
6869

6970
#endregion
@@ -95,7 +96,9 @@ public class OAuth2Client {
9596
}
9697

9798
_httpClient = httpClient;
98-
_apiTokenIssuer = credentialsConfig.Config.ApiTokenIssuer;
99+
var tokenEndpoint = BuildTokenEndpointUrl(credentialsConfig.Config.ApiTokenIssuer);
100+
_apiTokenIssuer = tokenEndpoint.BasePath;
101+
_apiTokenPath = tokenEndpoint.Path;
99102
_authRequest = new Dictionary<string, string> {
100103
{ "client_id", credentialsConfig.Config.ClientId },
101104
{ "client_secret", credentialsConfig.Config.ClientSecret },
@@ -118,8 +121,8 @@ public class OAuth2Client {
118121
private async Task ExchangeTokenAsync(CancellationToken cancellationToken = default) {
119122
var requestBuilder = new RequestBuilder<IDictionary<string, string>> {
120123
Method = HttpMethod.Post,
121-
BasePath = $"https://{_apiTokenIssuer}",
122-
PathTemplate = "/oauth/token",
124+
BasePath = _apiTokenIssuer,
125+
PathTemplate = _apiTokenPath,
123126
Body = _authRequest,
124127
ContentType = "application/x-www-form-urlencode"
125128
};
@@ -175,6 +178,13 @@ public class OAuth2Client {
175178
}
176179
}
177180

181+
private static (string BasePath, string Path) BuildTokenEndpointUrl(string issuer) {
182+
var uri = new Uri(issuer);
183+
return uri.AbsolutePath.Equals("/")
184+
? ($"{uri.Scheme}://{uri.Host}", "/oauth/token")
185+
: ($"{uri.Scheme}://{uri.Host}", uri.AbsolutePath);
186+
}
187+
178188
/// <summary>
179189
/// Gets the access token, and handles exchanging, rudimentary in memory caching and refreshing it when expired
180190
/// </summary>

config/clients/dotnet/template/Configuration_Credentials.mustache

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,24 @@ public interface IClientCredentialsConfig {
6363

6464
public interface ICredentialsConfig: IClientCredentialsConfig, IApiTokenConfig {}
6565

66-
public struct CredentialsConfig : ICredentialsConfig {
67-
public string? ClientId { get; set; }
68-
public string? ClientSecret { get; set; }
69-
public string? ApiTokenIssuer { get; set; }
70-
public string? ApiAudience { get; set; }
71-
public string? ApiToken { get; set; }
66+
public struct CredentialsConfig : ICredentialsConfig, IClientCredentialsConfig, IApiTokenConfig
67+
{
68+
private string? _tokenIssuer;
69+
70+
public string? ClientId { get; set; }
71+
72+
public string? ClientSecret { get; set; }
73+
74+
public string? ApiTokenIssuer {
75+
get => _tokenIssuer;
76+
set => _tokenIssuer = (!value?.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ?? false) && (!value?.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ?? false)
77+
? $"https://{value}"
78+
: value;
79+
}
80+
81+
public string? ApiAudience { get; set; }
82+
83+
public string? ApiToken { get; set; }
7284
}
7385

7486
public interface IAuthCredentialsConfig {
@@ -125,9 +137,8 @@ public class Credentials: IAuthCredentialsConfig {
125137
throw new FgaRequiredParamError("Configuration", nameof(Config.ApiAudience));
126138
}
127139

128-
if (!string.IsNullOrWhiteSpace(Config?.ApiTokenIssuer) && !IsWellFormedUriString($"https://{Config.ApiTokenIssuer}")) {
129-
throw new FgaValidationError(
130-
$"Configuration.ApiTokenIssuer does not form a valid URI (https://{Config.ApiTokenIssuer})");
140+
if (!IsWellFormedUriString(Config.ApiTokenIssuer)) {
141+
throw new FgaValidationError($"Configuration.ApiTokenIssuer does not form a valid URI ({Config.ApiTokenIssuer})");
131142
}
132143

133144
break;

config/clients/dotnet/template/api_test.mustache

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -118,28 +118,70 @@ namespace {{testPackageName}}.Api {
118118
exceptionMissingApiToken.Message);
119119
}
120120

121-
// /// <summary>
122-
// /// Test that the provided api token issuer is well-formed
123-
// /// </summary>
121+
/// <summary>
122+
/// Test that api token issuer value is normalized
123+
/// </summary>
124+
[Theory]
125+
[InlineData(null, null)]
126+
[InlineData("tokenissuer.fga.example", "https://tokenissuer.fga.example")]
127+
[InlineData("http://tokenissuer.fga.example", "http://tokenissuer.fga.example")]
128+
[InlineData("https://tokenissuer.fga.example", "https://tokenissuer.fga.example")]
129+
public void ApiTokenIssuerIsNormalized(string issuer, string expected) {
130+
var config = new CredentialsConfig() {
131+
ClientId = "some-id",
132+
ClientSecret = "some-secret",
133+
ApiTokenIssuer = issuer,
134+
ApiAudience = "some-audience",
135+
};
136+
137+
Assert.Equal(expected, config.ApiTokenIssuer);
138+
}
139+
140+
/// <summary>
141+
/// Test that providing malformed token issuer should error
142+
/// </summary>
124143
[Fact]
125-
public void ValidApiTokenIssuerWellFormed() {
144+
public void ApiTokenIssuerIsMalformed() {
126145
var config = new Configuration.Configuration() {
127146
ApiHost = _host,
128147
Credentials = new Credentials() {
129148
Method = CredentialsMethod.ClientCredentials,
130149
Config = new CredentialsConfig() {
131150
ClientId = "some-id",
132151
ClientSecret = "some-secret",
133-
ApiTokenIssuer = "https://tokenissuer.{{sampleApiDomain}}",
152+
ApiTokenIssuer = "file://tokenissuer.fga.example",
134153
ApiAudience = "some-audience",
135154
}
136155
}
137156
};
138157
void ActionMalformedApiTokenIssuer() => config.EnsureValid();
139158
var exception = Assert.Throws<FgaValidationError>(ActionMalformedApiTokenIssuer);
140-
Assert.Equal("Configuration.ApiTokenIssuer does not form a valid URI (https://https://tokenissuer.{{sampleApiDomain}})", exception.Message);
159+
Assert.Equal("Configuration.ApiTokenIssuer does not form a valid URI (https://file://tokenissuer.fga.example)", exception.Message);
141160
}
142161

162+
/// <summary>
163+
/// Test that the provided api token issuer is well-formed
164+
/// </summary>
165+
[Theory]
166+
[InlineData("tokenissuer.fga.example")]
167+
[InlineData("http://tokenissuer.fga.example")]
168+
[InlineData("https://tokenissuer.fga.example")]
169+
public void ValidApiTokenIssuerWellFormed(string issuer) {
170+
var config = new Configuration.Configuration() {
171+
ApiHost = _host,
172+
Credentials = new Credentials() {
173+
Method = CredentialsMethod.ClientCredentials,
174+
Config = new CredentialsConfig() {
175+
ClientId = "some-id",
176+
ClientSecret = "some-secret",
177+
ApiTokenIssuer = issuer,
178+
ApiAudience = "some-audience",
179+
}
180+
}
181+
};
182+
config.EnsureValid();
183+
}
184+
143185
/// <summary>
144186
/// Test that the authorization header is being sent
145187
/// </summary>
@@ -295,7 +337,7 @@ namespace {{testPackageName}}.Api {
295337
.SetupSequence<Task<HttpResponseMessage>>(
296338
"SendAsync",
297339
ItExpr.Is<HttpRequestMessage>(req =>
298-
req.RequestUri == new Uri($"https://{config.Credentials.Config.ApiTokenIssuer}/oauth/token") &&
340+
req.RequestUri == new Uri($"{config.Credentials.Config.ApiTokenIssuer}/oauth/token") &&
299341
req.Method == HttpMethod.Post &&
300342
req.Content.Headers.ContentType.ToString().Equals("application/x-www-form-urlencoded")),
301343
ItExpr.IsAny<CancellationToken>()
@@ -350,7 +392,7 @@ namespace {{testPackageName}}.Api {
350392
"SendAsync",
351393
Times.Exactly(1),
352394
ItExpr.Is<HttpRequestMessage>(req =>
353-
req.RequestUri == new Uri($"https://{config.Credentials.Config.ApiTokenIssuer}/oauth/token") &&
395+
req.RequestUri == new Uri($"{config.Credentials.Config.ApiTokenIssuer}/oauth/token") &&
354396
req.Method == HttpMethod.Post &&
355397
req.Content.Headers.ContentType.ToString().Equals("application/x-www-form-urlencoded")),
356398
ItExpr.IsAny<CancellationToken>()
@@ -394,7 +436,7 @@ namespace {{testPackageName}}.Api {
394436
.SetupSequence<Task<HttpResponseMessage>>(
395437
"SendAsync",
396438
ItExpr.Is<HttpRequestMessage>(req =>
397-
req.RequestUri == new Uri($"https://{config.Credentials.Config.ApiTokenIssuer}/oauth/token") &&
439+
req.RequestUri == new Uri($"{config.Credentials.Config.ApiTokenIssuer}/oauth/token") &&
398440
req.Method == HttpMethod.Post &&
399441
req.Content.Headers.ContentType.ToString().Equals("application/x-www-form-urlencoded")),
400442
ItExpr.IsAny<CancellationToken>()
@@ -449,7 +491,7 @@ namespace {{testPackageName}}.Api {
449491
"SendAsync",
450492
Times.Exactly(2),
451493
ItExpr.Is<HttpRequestMessage>(req =>
452-
req.RequestUri == new Uri($"https://{config.Credentials.Config.ApiTokenIssuer}/oauth/token") &&
494+
req.RequestUri == new Uri($"{config.Credentials.Config.ApiTokenIssuer}/oauth/token") &&
453495
req.Method == HttpMethod.Post &&
454496
req.Content.Headers.ContentType.ToString().Equals("application/x-www-form-urlencoded")),
455497
ItExpr.IsAny<CancellationToken>()
@@ -493,7 +535,7 @@ namespace {{testPackageName}}.Api {
493535
.SetupSequence<Task<HttpResponseMessage>>(
494536
"SendAsync",
495537
ItExpr.Is<HttpRequestMessage>(req =>
496-
req.RequestUri == new Uri($"https://{config.Credentials.Config.ApiTokenIssuer}/oauth/token") &&
538+
req.RequestUri == new Uri($"{config.Credentials.Config.ApiTokenIssuer}/oauth/token") &&
497539
req.Method == HttpMethod.Post &&
498540
req.Content.Headers.ContentType.ToString().Equals("application/x-www-form-urlencoded")),
499541
ItExpr.IsAny<CancellationToken>()
@@ -545,7 +587,7 @@ namespace {{testPackageName}}.Api {
545587
"SendAsync",
546588
Times.Exactly(3),
547589
ItExpr.Is<HttpRequestMessage>(req =>
548-
req.RequestUri == new Uri($"https://{config.Credentials.Config.ApiTokenIssuer}/oauth/token") &&
590+
req.RequestUri == new Uri($"{config.Credentials.Config.ApiTokenIssuer}/oauth/token") &&
549591
req.Method == HttpMethod.Post &&
550592
req.Content.Headers.ContentType.ToString().Equals("application/x-www-form-urlencoded")),
551593
ItExpr.IsAny<CancellationToken>()

0 commit comments

Comments
 (0)