Skip to content

Commit 27a29da

Browse files
committed
Incorporate the changes committed in the dev branch
2 parents ef1d1f0 + dea5be9 commit 27a29da

File tree

4 files changed

+191
-126
lines changed

4 files changed

+191
-126
lines changed

src/AspNet.Security.OpenId/OpenIdAuthenticationConfiguration.cs

Lines changed: 172 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
using System;
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OpenId.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using System;
8+
using System.Diagnostics;
29
using System.Globalization;
310
using System.Linq;
411
using System.Net.Http;
@@ -7,7 +14,6 @@
714
using System.Threading.Tasks;
815
using System.Xml;
916
using System.Xml.Linq;
10-
using AngleSharp.Dom.Html;
1117
using AngleSharp.Parser.Html;
1218
using JetBrains.Annotations;
1319
using Microsoft.IdentityModel.Protocols;
@@ -76,148 +82,199 @@ public Retriever([NotNull] HttpClient client, [NotNull] HtmlParser parser)
7682
public async Task<OpenIdAuthenticationConfiguration> GetConfigurationAsync(
7783
[NotNull] string address, [NotNull] IDocumentRetriever retriever, CancellationToken cancellationToken)
7884
{
85+
if (retriever == null)
86+
{
87+
throw new ArgumentNullException(nameof(retriever));
88+
}
89+
7990
if (string.IsNullOrEmpty(address))
8091
{
8192
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
8293
}
8394

84-
if (retriever == null)
95+
if (!Uri.TryCreate(address, UriKind.Absolute, out Uri uri))
8596
{
86-
throw new ArgumentNullException(nameof(retriever));
97+
throw new ArgumentException("The address must be an absolute URI.", nameof(address));
8798
}
8899

89100
using (var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
90101
{
91-
// If the final authentication endpoint cannot be found after 30 seconds, abort the discovery operation.
92-
cancellationTokenSource.CancelAfter(HttpClient.Timeout < TimeSpan.FromSeconds(30) ?
93-
HttpClient.Timeout : TimeSpan.FromSeconds(30));
102+
cancellationTokenSource.CancelAfter(HttpClient.Timeout);
94103

95-
do
104+
return await DiscoverConfigurationAsync(uri, cancellationTokenSource.Token);
105+
}
106+
}
107+
108+
private async Task<OpenIdAuthenticationConfiguration> DiscoverConfigurationAsync(
109+
[NotNull] Uri address, CancellationToken cancellationToken)
110+
{
111+
Debug.Assert(address != null, "The address shouldn't be null or empty.");
112+
113+
while (!cancellationToken.IsCancellationRequested)
114+
{
115+
// application/xrds+xml MUST be the preferred content type to avoid a second round-trip.
116+
// See http://openid.net/specs/yadis-v1.0.pdf (chapter 6.2.4)
117+
var request = new HttpRequestMessage(HttpMethod.Get, address);
118+
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(OpenIdAuthenticationConstants.Media.Xrds));
119+
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(OpenIdAuthenticationConstants.Media.Html));
120+
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(OpenIdAuthenticationConstants.Media.Xhtml));
121+
122+
var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
123+
if (!response.IsSuccessStatusCode)
96124
{
97-
// application/xrds+xml MUST be the preferred content type to avoid a second round-trip.
98-
// See http://openid.net/specs/yadis-v1.0.pdf (chapter 6.2.4)
99-
var request = new HttpRequestMessage(HttpMethod.Get, address);
100-
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(OpenIdAuthenticationConstants.Media.Xrds));
101-
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(OpenIdAuthenticationConstants.Media.Html));
102-
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(OpenIdAuthenticationConstants.Media.Xhtml));
103-
104-
var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationTokenSource.Token);
105-
if (!response.IsSuccessStatusCode)
106-
{
107-
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
108-
"The Yadis discovery failed because an invalid response was received: the identity provider " +
109-
"returned returned a {0} response with the following payload: {1} {2}.",
110-
/* Status: */ response.StatusCode,
111-
/* Headers: */ response.Headers.ToString(),
112-
/* Body: */ await response.Content.ReadAsStringAsync()));
113-
}
125+
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
126+
"The Yadis discovery failed because an invalid response was received: the identity provider " +
127+
"returned returned a {0} response with the following payload: {1} {2}.",
128+
/* Status: */ response.StatusCode,
129+
/* Headers: */ response.Headers.ToString(),
130+
/* Body: */ await response.Content.ReadAsStringAsync()));
131+
}
114132

115-
// Note: application/xrds+xml is the standard content type but text/xml is frequent.
116-
// See http://openid.net/specs/yadis-v1.0.pdf (chapter 6.2.6)
117-
var media = response.Content.Headers.ContentType?.MediaType;
118-
if (string.Equals(media, OpenIdAuthenticationConstants.Media.Xrds, StringComparison.OrdinalIgnoreCase) ||
119-
string.Equals(media, OpenIdAuthenticationConstants.Media.Xml, StringComparison.OrdinalIgnoreCase))
133+
// Note: application/xrds+xml is the standard content type but text/xml is frequent.
134+
// See http://openid.net/specs/yadis-v1.0.pdf (chapter 6.2.6)
135+
var media = response.Content.Headers.ContentType?.MediaType;
136+
if (string.Equals(media, OpenIdAuthenticationConstants.Media.Xrds, StringComparison.OrdinalIgnoreCase) ||
137+
string.Equals(media, OpenIdAuthenticationConstants.Media.Xml, StringComparison.OrdinalIgnoreCase))
138+
{
139+
var endpoint = await ProcessXrdsDocumentAsync(response, cancellationToken);
140+
if (endpoint == null)
120141
{
121-
using (var stream = await response.Content.ReadAsStreamAsync())
122-
using (var reader = XmlReader.Create(stream))
123-
{
124-
var document = XDocument.Load(reader);
125-
126-
var endpoint = (from service in document.Root.Element(XName.Get("XRD", "xri://$xrd*($v*2.0)"))
127-
.Descendants(XName.Get("Service", "xri://$xrd*($v*2.0)"))
128-
where service.Descendants(XName.Get("Type", "xri://$xrd*($v*2.0)"))
129-
.Any(type => type.Value == "http://specs.openid.net/auth/2.0/server")
130-
orderby service.Attribute("priority")?.Value
131-
select service.Element(XName.Get("URI", "xri://$xrd*($v*2.0)"))?.Value).FirstOrDefault();
132-
133-
Uri uri;
134-
if (!string.IsNullOrEmpty(endpoint) && Uri.TryCreate(endpoint, UriKind.Absolute, out uri))
135-
{
136-
return new OpenIdAuthenticationConfiguration
137-
{
138-
AuthenticationEndpoint = uri.AbsoluteUri
139-
};
140-
}
141-
142-
throw new InvalidOperationException(
143-
"The Yadis discovery failed because the XRDS document returned by the " +
144-
"identity provider was invalid or didn't contain the endpoint address.");
145-
}
142+
throw new InvalidOperationException(
143+
"The Yadis discovery failed because the XRDS document returned by the " +
144+
"identity provider didn't contain the authentication endpoint address.");
146145
}
147146

148-
// Try to extract the XRDS location from the response headers before parsing the body.
149-
// See http://openid.net/specs/yadis-v1.0.pdf (chapter 6.2.6)
150-
var location = (from header in response.Headers
151-
where string.Equals(header.Key, OpenIdAuthenticationConstants.Headers.XrdsLocation, StringComparison.OrdinalIgnoreCase)
152-
from value in header.Value
153-
select value).FirstOrDefault();
154-
155-
if (!string.IsNullOrEmpty(location))
147+
return new OpenIdAuthenticationConfiguration
156148
{
157-
Uri uri;
158-
if (!Uri.TryCreate(location, UriKind.Absolute, out uri))
159-
{
160-
throw new InvalidOperationException(
161-
"The Yadis discovery failed because the X-XRDS-Location " +
162-
"header returned by the identity provider was invalid.");
163-
}
149+
AuthenticationEndpoint = endpoint.AbsoluteUri
150+
};
151+
}
164152

165-
// Retry the discovery operation, but using the XRDS location extracted from the header.
166-
address = uri.AbsoluteUri;
153+
// Try to extract the XRDS location from the response headers before parsing the body.
154+
// See http://openid.net/specs/yadis-v1.0.pdf (chapter 6.2.6)
155+
if (response.Headers.Contains(OpenIdAuthenticationConstants.Headers.XrdsLocation))
156+
{
157+
var location = ProcessUnknownDocument(response);
158+
if (location != null)
159+
{
160+
// Retry the discovery operation, but using the
161+
// XRDS location extracted from the header.
162+
address = location;
167163

168164
continue;
169165
}
166+
}
170167

171-
// Only text/html or application/xhtml+xml can be safely parsed.
172-
// See http://openid.net/specs/yadis-v1.0.pdf
173-
if (string.Equals(media, OpenIdAuthenticationConstants.Media.Html, StringComparison.OrdinalIgnoreCase) ||
174-
string.Equals(media, OpenIdAuthenticationConstants.Media.Xhtml, StringComparison.OrdinalIgnoreCase))
168+
// Only text/html or application/xhtml+xml can be safely parsed.
169+
// See http://openid.net/specs/yadis-v1.0.pdf
170+
if (string.Equals(media, OpenIdAuthenticationConstants.Media.Html, StringComparison.OrdinalIgnoreCase) ||
171+
string.Equals(media, OpenIdAuthenticationConstants.Media.Xhtml, StringComparison.OrdinalIgnoreCase))
172+
{
173+
var location = await ProcessHtmlDocumentAsync(response, cancellationToken);
174+
if (location != null)
175175
{
176-
IHtmlDocument document = null;
177-
178-
try
179-
{
180-
using (var stream = await response.Content.ReadAsStreamAsync())
181-
{
182-
document = await HtmlParser.ParseAsync(stream, cancellationTokenSource.Token);
183-
}
184-
}
185-
186-
catch (Exception exception)
187-
{
188-
throw new InvalidOperationException("An exception occurred while parsing the HTML document.", exception);
189-
}
190-
191-
var endpoint = (from element in document.Head.GetElementsByTagName(OpenIdAuthenticationConstants.Metadata.Meta)
192-
let attribute = element.Attributes[OpenIdAuthenticationConstants.Metadata.HttpEquiv]
193-
where string.Equals(attribute?.Value, OpenIdAuthenticationConstants.Metadata.XrdsLocation, StringComparison.OrdinalIgnoreCase)
194-
select element.Attributes[OpenIdAuthenticationConstants.Metadata.Content]?.Value).FirstOrDefault();
195-
196-
if (!string.IsNullOrEmpty(endpoint))
197-
{
198-
Uri uri;
199-
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out uri))
200-
{
201-
throw new InvalidOperationException(
202-
"The Yadis discovery failed because the X-XRDS-Location " +
203-
"metadata returned by the identity provider was invalid.");
204-
}
205-
206-
// Retry the discovery operation, but using the XRDS
207-
// location extracted from the parsed HTML document.
208-
address = uri.AbsoluteUri;
209-
210-
continue;
211-
}
176+
// Retry the discovery operation, but using the
177+
// XRDS location extracted from the HTML document.
178+
address = location;
179+
180+
continue;
212181
}
182+
}
183+
}
184+
185+
throw new InvalidOperationException("The Yadis discovery failed because the XRDS document location was not found.");
186+
}
187+
188+
private async Task<Uri> ProcessXrdsDocumentAsync(
189+
[NotNull] HttpResponseMessage response, CancellationToken cancellationToken)
190+
{
191+
Debug.Assert(response != null, "The HTTP response shouldn't be null.");
213192

214-
throw new InvalidOperationException("The Yadis discovery failed because the XRDS document location was not found.");
193+
// Abort the operation if cancellation was requested.
194+
cancellationToken.ThrowIfCancellationRequested();
195+
196+
using (var stream = await response.Content.ReadAsStreamAsync())
197+
using (var reader = XmlReader.Create(stream))
198+
{
199+
var document = XDocument.Load(reader);
200+
201+
var endpoint = (from service in document.Root.Element(XName.Get("XRD", "xri://$xrd*($v*2.0)"))
202+
.Descendants(XName.Get("Service", "xri://$xrd*($v*2.0)"))
203+
where service.Descendants(XName.Get("Type", "xri://$xrd*($v*2.0)"))
204+
.Any(type => type.Value == "http://specs.openid.net/auth/2.0/server")
205+
orderby service.Attribute("priority")?.Value
206+
select service.Element(XName.Get("URI", "xri://$xrd*($v*2.0)"))?.Value).FirstOrDefault();
207+
208+
if (string.IsNullOrEmpty(endpoint))
209+
{
210+
return null;
211+
}
212+
213+
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out Uri uri))
214+
{
215+
throw new InvalidOperationException(
216+
"The Yadis discovery failed because the XRDS document " +
217+
"returned by the identity provider was invalid.");
215218
}
216219

217-
while (!cancellationTokenSource.IsCancellationRequested);
220+
return uri;
221+
}
222+
}
223+
224+
private Uri ProcessUnknownDocument([NotNull] HttpResponseMessage response)
225+
{
226+
Debug.Assert(response != null, "The HTTP response shouldn't be null.");
227+
228+
var endpoint = (from header in response.Headers
229+
where string.Equals(header.Key, OpenIdAuthenticationConstants.Headers.XrdsLocation, StringComparison.OrdinalIgnoreCase)
230+
from value in header.Value
231+
select value).FirstOrDefault();
232+
233+
if (string.IsNullOrEmpty(endpoint))
234+
{
235+
return null;
236+
}
237+
238+
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out Uri uri))
239+
{
240+
throw new InvalidOperationException(
241+
"The Yadis discovery failed because the X-XRDS-Location " +
242+
"header returned by the identity provider was invalid.");
218243
}
219244

220-
throw new InvalidOperationException("The OpenID 2.0 configuration cannot be retrieved.");
245+
return uri;
246+
}
247+
248+
private async Task<Uri> ProcessHtmlDocumentAsync(
249+
[NotNull] HttpResponseMessage response, CancellationToken cancellationToken)
250+
{
251+
Debug.Assert(response != null, "The HTTP response shouldn't be null.");
252+
253+
// Abort the operation if cancellation was requested.
254+
cancellationToken.ThrowIfCancellationRequested();
255+
256+
using (var stream = await response.Content.ReadAsStreamAsync())
257+
using (var document = await HtmlParser.ParseAsync(stream, cancellationToken))
258+
{
259+
var endpoint = (from element in document.Head.GetElementsByTagName(OpenIdAuthenticationConstants.Metadata.Meta)
260+
let attribute = element.Attributes[OpenIdAuthenticationConstants.Metadata.HttpEquiv]
261+
where string.Equals(attribute?.Value, OpenIdAuthenticationConstants.Metadata.XrdsLocation, StringComparison.OrdinalIgnoreCase)
262+
select element.Attributes[OpenIdAuthenticationConstants.Metadata.Content]?.Value).FirstOrDefault();
263+
264+
if (string.IsNullOrEmpty(endpoint))
265+
{
266+
return null;
267+
}
268+
269+
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out Uri uri))
270+
{
271+
throw new InvalidOperationException(
272+
"The Yadis discovery failed because the X-XRDS-Location " +
273+
"metadata returned by the identity provider was invalid.");
274+
}
275+
276+
return uri;
277+
}
221278
}
222279
}
223280
}

0 commit comments

Comments
 (0)