Skip to content

Commit f4d5707

Browse files
committed
enforcing URI-ness of SecurityScheme URLs
1 parent 6eac890 commit f4d5707

File tree

2 files changed

+95
-92
lines changed

2 files changed

+95
-92
lines changed

src/A2A/Models/SecurityScheme.cs

Lines changed: 91 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,14 @@ public sealed class OAuth2SecurityScheme(OAuthFlows flows, string? description =
108108
/// </remarks>
109109
/// <param name="openIdConnectUrl">Well-known URL to discover the [[OpenID-Connect-Discovery]] provider metadata.</param>
110110
/// <param name="description">A short description for the security scheme. CommonMark syntax MAY be used for rich text representation.</param>
111-
public sealed class OpenIdConnectSecurityScheme(string openIdConnectUrl, string? description = null) : SecurityScheme(description)
111+
public sealed class OpenIdConnectSecurityScheme(Uri openIdConnectUrl, string? description = null) : SecurityScheme(description)
112112
{
113113
/// <summary>
114114
/// Well-known URL to discover the [[OpenID-Connect-Discovery]] provider metadata.
115115
/// </summary>
116116
[JsonPropertyName("openIdConnectUrl")]
117117
[JsonRequired]
118-
public string OpenIdConnectUrl { get; init; } = openIdConnectUrl;
118+
public Uri OpenIdConnectUrl { get; init; } = openIdConnectUrl;
119119
}
120120

121121
/// <summary>
@@ -163,48 +163,25 @@ public sealed class OAuthFlows
163163
}
164164

165165
/// <summary>
166-
/// Configuration details for a supported OAuth Authorization Code Flow.
166+
/// Configuration details applicable to all OAuth Flows.
167167
/// </summary>
168168
/// <remarks>
169-
/// Initializes a new instance of the <see cref="AuthorizationCodeOAuthFlow"/> class.
169+
/// Initializes a new instance of the <see cref="BaseOauthFlow"/> class.
170170
/// </remarks>
171-
/// <param name="authorizationUrl">
172-
/// The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
173-
/// </param>
174-
/// <param name="tokenUrl">
175-
/// The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
176-
/// </param>
177171
/// <param name="scopes">
178172
/// The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty.
179173
/// </param>
180174
/// <param name="refreshUrl">
181175
/// The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
182176
/// </param>
183-
public sealed class AuthorizationCodeOAuthFlow(
184-
string authorizationUrl,
185-
string tokenUrl,
186-
IDictionary<string, string> scopes,
187-
string? refreshUrl = null)
177+
public abstract class BaseOauthFlow(IDictionary<string, string> scopes,
178+
Uri? refreshUrl = null)
188179
{
189-
/// <summary>
190-
/// The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
191-
/// </summary>
192-
[JsonPropertyName("authorizationUrl")]
193-
[JsonRequired]
194-
public string AuthorizationUrl { get; init; } = authorizationUrl;
195-
196-
/// <summary>
197-
/// The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
198-
/// </summary>
199-
[JsonPropertyName("tokenUrl")]
200-
[JsonRequired]
201-
public string TokenUrl { get; init; } = tokenUrl;
202-
203180
/// <summary>
204181
/// The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
205182
/// </summary>
206183
[JsonPropertyName("refreshUrl")]
207-
public string? RefreshUrl { get; init; } = refreshUrl;
184+
public Uri? RefreshUrl { get; init; } = refreshUrl;
208185

209186
/// <summary>
210187
/// The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty.
@@ -215,10 +192,10 @@ public sealed class AuthorizationCodeOAuthFlow(
215192
}
216193

217194
/// <summary>
218-
/// Configuration details for a supported OAuth Client Credentials Flow.
195+
/// Configuration details for an OAuth Flow requiring a Token URL.
219196
/// </summary>
220197
/// <remarks>
221-
/// Initializes a new instance of the <see cref="ClientCredentialsOAuthFlow"/> class.
198+
/// Initializes a new instance of the <see cref="TokenUrlOauthFlow"/> class.
222199
/// </remarks>
223200
/// <param name="tokenUrl">
224201
/// The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
@@ -229,38 +206,54 @@ public sealed class AuthorizationCodeOAuthFlow(
229206
/// <param name="refreshUrl">
230207
/// The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
231208
/// </param>
232-
public sealed class ClientCredentialsOAuthFlow(
233-
string tokenUrl,
209+
public abstract class TokenUrlOauthFlow(Uri tokenUrl,
234210
IDictionary<string, string> scopes,
235-
string? refreshUrl = null)
211+
Uri? refreshUrl = null) : BaseOauthFlow(scopes, refreshUrl)
236212
{
237213
/// <summary>
238214
/// The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
239215
/// </summary>
240216
[JsonPropertyName("tokenUrl")]
241217
[JsonRequired]
242-
public string TokenUrl { get; init; } = tokenUrl;
243-
244-
/// <summary>
245-
/// The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
246-
/// </summary>
247-
[JsonPropertyName("refreshUrl")]
248-
public string? RefreshUrl { get; init; } = refreshUrl;
218+
public Uri TokenUrl { get; init; } = tokenUrl;
219+
}
249220

221+
/// <summary>
222+
/// Configuration details for an OAuth Flow requiring an Authorization URL.
223+
/// </summary>
224+
/// <remarks>
225+
/// Initializes a new instance of the <see cref="AuthUrlOauthFlow"/> class.
226+
/// </remarks>
227+
/// <param name="authorizationUrl">
228+
/// The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
229+
/// </param>
230+
/// <param name="scopes">
231+
/// The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty.
232+
/// </param>
233+
/// <param name="refreshUrl">
234+
/// The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
235+
/// </param>
236+
public abstract class AuthUrlOauthFlow(Uri authorizationUrl,
237+
IDictionary<string, string> scopes,
238+
Uri? refreshUrl = null) : BaseOauthFlow(scopes, refreshUrl)
239+
{
250240
/// <summary>
251-
/// The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty.
241+
/// The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
252242
/// </summary>
253-
[JsonPropertyName("scopes")]
243+
[JsonPropertyName("authorizationUrl")]
254244
[JsonRequired]
255-
public IDictionary<string, string> Scopes { get; init; } = scopes;
245+
public Uri AuthorizationUrl { get; init; } = authorizationUrl;
256246
}
257247

258248
/// <summary>
259-
/// Configuration details for a supported OAuth Resource Owner Password Flow.
249+
/// Configuration details for a supported OAuth Authorization Code Flow.
260250
/// </summary>
261251
/// <remarks>
262-
/// Initializes a new instance of the <see cref="PasswordOAuthFlow"/> class.
252+
/// Initializes a new instance of the <see cref="AuthorizationCodeOAuthFlow"/> class.
263253
/// </remarks>
254+
/// <param name="authorizationUrl">
255+
/// The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
256+
/// </param>
264257
/// <param name="tokenUrl">
265258
/// The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
266259
/// </param>
@@ -270,31 +263,61 @@ public sealed class ClientCredentialsOAuthFlow(
270263
/// <param name="refreshUrl">
271264
/// The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
272265
/// </param>
273-
public sealed class PasswordOAuthFlow(
274-
string tokenUrl,
266+
public sealed class AuthorizationCodeOAuthFlow(
267+
Uri authorizationUrl,
268+
Uri tokenUrl,
275269
IDictionary<string, string> scopes,
276-
string? refreshUrl = null)
270+
Uri? refreshUrl = null) : TokenUrlOauthFlow(tokenUrl, scopes, refreshUrl)
277271
{
278272
/// <summary>
279-
/// The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
273+
/// The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
280274
/// </summary>
281-
[JsonPropertyName("tokenUrl")]
275+
[JsonPropertyName("authorizationUrl")]
282276
[JsonRequired]
283-
public string TokenUrl { get; init; } = tokenUrl;
277+
public Uri AuthorizationUrl { get; init; } = authorizationUrl;
278+
}
284279

285-
/// <summary>
286-
/// The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
287-
/// </summary>
288-
[JsonPropertyName("refreshUrl")]
289-
public string? RefreshUrl { get; init; } = refreshUrl;
280+
/// <summary>
281+
/// Configuration details for a supported OAuth Client Credentials Flow.
282+
/// </summary>
283+
/// <remarks>
284+
/// Initializes a new instance of the <see cref="ClientCredentialsOAuthFlow"/> class.
285+
/// </remarks>
286+
/// <param name="tokenUrl">
287+
/// The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
288+
/// </param>
289+
/// <param name="scopes">
290+
/// The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty.
291+
/// </param>
292+
/// <param name="refreshUrl">
293+
/// The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
294+
/// </param>
295+
public sealed class ClientCredentialsOAuthFlow(
296+
Uri tokenUrl,
297+
IDictionary<string, string> scopes,
298+
Uri? refreshUrl = null) : TokenUrlOauthFlow(tokenUrl, scopes, refreshUrl)
299+
{ }
290300

291-
/// <summary>
292-
/// The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty.
293-
/// </summary>
294-
[JsonPropertyName("scopes")]
295-
[JsonRequired]
296-
public IDictionary<string, string> Scopes { get; init; } = scopes;
297-
}
301+
/// <summary>
302+
/// Configuration details for a supported OAuth Resource Owner Password Flow.
303+
/// </summary>
304+
/// <remarks>
305+
/// Initializes a new instance of the <see cref="PasswordOAuthFlow"/> class.
306+
/// </remarks>
307+
/// <param name="tokenUrl">
308+
/// The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
309+
/// </param>
310+
/// <param name="scopes">
311+
/// The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty.
312+
/// </param>
313+
/// <param name="refreshUrl">
314+
/// The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
315+
/// </param>
316+
public sealed class PasswordOAuthFlow(
317+
Uri tokenUrl,
318+
IDictionary<string, string> scopes,
319+
Uri? refreshUrl = null) : TokenUrlOauthFlow(tokenUrl, scopes, refreshUrl)
320+
{ }
298321

299322
/// <summary>
300323
/// Configuration details for a supported OAuth Implicit Flow.
@@ -312,27 +335,7 @@ public sealed class PasswordOAuthFlow(
312335
/// The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
313336
/// </param>
314337
public sealed class ImplicitOAuthFlow(
315-
string authorizationUrl,
338+
Uri authorizationUrl,
316339
IDictionary<string, string> scopes,
317-
string? refreshUrl = null)
318-
{
319-
/// <summary>
320-
/// The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
321-
/// </summary>
322-
[JsonPropertyName("authorizationUrl")]
323-
[JsonRequired]
324-
public string AuthorizationUrl { get; init; } = authorizationUrl;
325-
326-
/// <summary>
327-
/// The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
328-
/// </summary>
329-
[JsonPropertyName("refreshUrl")]
330-
public string? RefreshUrl { get; init; } = refreshUrl;
331-
332-
/// <summary>
333-
/// The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty.
334-
/// </summary>
335-
[JsonPropertyName("scopes")]
336-
[JsonRequired]
337-
public IDictionary<string, string> Scopes { get; init; } = scopes;
338-
}
340+
Uri? refreshUrl = null) : AuthUrlOauthFlow(authorizationUrl, scopes, refreshUrl)
341+
{ }

tests/A2A.UnitTests/Models/SecuritySchemeTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public void OAuth2SecurityScheme_SerializesAndDeserializesCorrectly()
8787
// Arrange
8888
var flows = new OAuthFlows
8989
{
90-
Password = new("https://example.com/token", scopes: new Dictionary<string, string>() { ["read"] = "Read access", ["write"] = "Write access" }),
90+
Password = new(new("https://example.com/token"), scopes: new Dictionary<string, string>() { ["read"] = "Read access", ["write"] = "Write access" }),
9191
};
9292
SecurityScheme scheme = new OAuth2SecurityScheme(flows, "OAuth2 authentication");
9393

@@ -105,7 +105,7 @@ public void OAuth2SecurityScheme_SerializesAndDeserializesCorrectly()
105105
Assert.Null(deserialized.Flows.Implicit);
106106
Assert.Null(deserialized.Flows.AuthorizationCode);
107107
Assert.NotNull(deserialized.Flows.Password);
108-
Assert.Equal("https://example.com/token", deserialized.Flows.Password.TokenUrl);
108+
Assert.Equal("https://example.com/token", deserialized.Flows.Password.TokenUrl.ToString());
109109
Assert.NotNull(deserialized.Flows.Password.Scopes);
110110
Assert.Equal(2, deserialized.Flows.Password.Scopes.Count);
111111
Assert.Contains("read", deserialized.Flows.Password.Scopes.Keys);
@@ -118,7 +118,7 @@ public void OAuth2SecurityScheme_SerializesAndDeserializesCorrectly()
118118
public void OpenIdConnectSecurityScheme_SerializesAndDeserializesCorrectly()
119119
{
120120
// Arrange
121-
SecurityScheme scheme = new OpenIdConnectSecurityScheme("https://example.com/.well-known/openid_configuration", "OpenID Connect authentication");
121+
SecurityScheme scheme = new OpenIdConnectSecurityScheme(new("https://example.com/.well-known/openid_configuration"), "OpenID Connect authentication");
122122

123123
// Act
124124
var json = JsonSerializer.Serialize(scheme, s_jsonOptions);
@@ -129,7 +129,7 @@ public void OpenIdConnectSecurityScheme_SerializesAndDeserializesCorrectly()
129129
Assert.Contains("\"description\": \"OpenID Connect authentication\"", json);
130130
Assert.NotNull(deserialized);
131131
Assert.Equal("OpenID Connect authentication", deserialized.Description);
132-
Assert.Equal("https://example.com/.well-known/openid_configuration", deserialized.OpenIdConnectUrl);
132+
Assert.Equal("https://example.com/.well-known/openid_configuration", deserialized.OpenIdConnectUrl.ToString());
133133
}
134134

135135
[Fact]

0 commit comments

Comments
 (0)