diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2010-04-16 16:38:06 -0700 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2010-04-16 16:38:06 -0700 |
commit | a23fa871be7ba3148c3182d2cfc5f89fbde0e895 (patch) | |
tree | e57e94f071189eb9a146047130ab6c9b230f739b | |
parent | a195843e69344d977013421d0ae5de78750b018c (diff) | |
parent | 34080bfd3948a0dea0dac6d876a200a9a8017669 (diff) | |
download | DotNetOpenAuth-a23fa871be7ba3148c3182d2cfc5f89fbde0e895.zip DotNetOpenAuth-a23fa871be7ba3148c3182d2cfc5f89fbde0e895.tar.gz DotNetOpenAuth-a23fa871be7ba3148c3182d2cfc5f89fbde0e895.tar.bz2 |
Merged in v3.4.3.
68 files changed, 1590 insertions, 402 deletions
diff --git a/doc/specs/openid_ui_extension_draft01.html b/doc/specs/openid-ui-extension.html index 71467f7..3327853 100644 --- a/doc/specs/openid_ui_extension_draft01.html +++ b/doc/specs/openid-ui-extension.html @@ -1,11 +1,11 @@ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> -<html lang="en"><head> -<title>Implementers' Draft: OpenID User Interface Extension 1.0 - DRAFT 0.4</title> -<meta http-equiv="Expires" content="Tue, 19 May 2009 02:37:11 +0000"> -<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> -<meta name="description" content="OpenID User Interface Extension 1.0 - DRAFT 0.4"> +<!-- test --> +<html lang="en"><head><title>Implementers' Draft: OpenID User Interface Extension 1.0 - DRAFT 0.5</title> +<meta http-equiv="Expires" content="Tue, 23 Jun 2009 20:52:02 +0000"> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<meta name="description" content="OpenID User Interface Extension 1.0 - DRAFT 0.5"> <meta name="generator" content="xml2rfc v1.33 (http://xml.resource.org/)"> -<style type="text/css"><!-- +<style type='text/css'><!-- body { font-family: verdana, charcoal, helvetica, arial, sans-serif; font-size: small; color: #000; background-color: #FFF; @@ -139,16 +139,17 @@ color: #CCC; background-color: #CCC; } --></style> -</head><body> -<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table> -<table summary="layout" border="0" cellpadding="0" cellspacing="0" width="66%"><tbody><tr><td><table summary="layout" border="0" cellpadding="2" cellspacing="1" width="100%"> -<tbody><tr><td class="header">Implementers' Draft</td><td class="header">A. Tom</td></tr> +</head> +<body> +<table summary="layout" cellpadding="0" cellspacing="2" class="TOCbug" align="right"><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></table> +<table summary="layout" width="66%" border="0" cellpadding="0" cellspacing="0"><tr><td><table summary="layout" width="100%" border="0" cellpadding="2" cellspacing="1"> +<tr><td class="header">Implementers' Draft</td><td class="header">A. Tom</td></tr> <tr><td class="header"> </td><td class="header">Yahoo!</td></tr> <tr><td class="header"> </td><td class="header">B. de Medeiros</td></tr> <tr><td class="header"> </td><td class="header">Google</td></tr> <tr><td class="header"> </td><td class="header">May 18, 2009</td></tr> -</tbody></table></td></tr></tbody></table> -<h1><br>OpenID User Interface Extension 1.0 - DRAFT 0.4</h1> +</table></td></tr></table> +<h1><br />OpenID User Interface Extension 1.0 - DRAFT 0.5</h1> <h3>Abstract</h3> @@ -156,53 +157,57 @@ This specification defines a mechanism to support OpenID user interfaces optimized for different environments and languages. -</p><a name="toc"></a><br><hr> +</p><a name="toc"></a><br /><hr /> <h3>Table of Contents</h3> <p class="toc"> <a href="#conv">1.</a> -Notation and Conventions<br> +Notation and Conventions<br /> <a href="#anchor1">2.</a> -Overview<br> +Overview<br /> <a href="#anchor2">3.</a> -Extension Namespace<br> +Extension Namespace<br /> <a href="#anchor3">4.</a> -Language Preference<br> +Language Preference<br /> <a href="#anchor4">5.</a> -Requesting Authentication in a Popup<br> +Requesting Authentication in a Popup<br /> <a href="#anchor5">5.1.</a> -Authentication Response in a Fragment<br> +Authentication Response in a Fragment<br /> <a href="#anchor6">6.</a> -Discovery<br> +Requesting Display of RP icons in the OP Approval UI<br /> <a href="#anchor7">7.</a> -Considerations<br> +Discovery<br /> <a href="#anchor8">8.</a> -Acknowledgements<br> -<a href="#rfc.references1">9.</a> -References<br> -<a href="#rfc.authors">§</a> -Authors' Addresses<br> +Considerations<br /> +<a href="#anchor9">9.</a> +Acknowledgements<br /> +<a href="#rfc.references1">10.</a> +References<br /> +<a href="#anchor11">Appendix A.</a> +Example Use of Experimental Mode<br /> +<a href="#rfc.authors">§</a> +Authors' Addresses<br /> </p> -<br clear="all"> +<br clear="all" /> -<a name="conv"></a><br><hr> -<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table> +<a name="conv"></a><br /><hr /> +<table summary="layout" cellpadding="0" cellspacing="2" class="TOCbug" align="right"><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></table> <a name="rfc.section.1"></a><h3>1. Notation and Conventions</h3> <p>The keywords MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as - described in <a class="info" href="#RFC2119">[RFC2119]<span> (</span><span class="info">Bradner, B., “Key words for use in RFCs to Indicate Requirement Levels,” .</span><span>)</span></a>. + described in <a class='info' href='#RFC2119'>[RFC2119]<span> (</span><span class='info'>Bradner, B., “Key words for use in RFCs to Indicate Requirement Levels,” .</span><span>)</span></a>. </p> <p> Unless otherwise noted, this specification is written as a direct - continuation of <a class="info" href="#OpenID%202.0">[OpenID 2.0]<span> (</span><span class="info">OpenID 2.0 Workgroup, “OpenID 2.0,” .</span><span>)</span></a>, inheriting the definitions and + continuation of <a class='info' href='#OpenID 2.0'>[OpenID 2.0]<span> (</span><span class='info'>OpenID 2.0 Workgroup, “OpenID 2.0,” .</span><span>)</span></a>, inheriting the definitions and guidelines set by it. </p> -<a name="anchor1"></a><br><hr> -<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table> +<a name="anchor1"></a><br /><hr /> +<table summary="layout" cellpadding="0" cellspacing="2" class="TOCbug" align="right"><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></table> <a name="rfc.section.2"></a><h3>2. Overview</h3> @@ -231,17 +236,18 @@ Overview</h3> to the user. </p> -<a name="anchor2"></a><br><hr> -<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table> +<a name="anchor2"></a><br /><hr /> +<table summary="layout" cellpadding="0" cellspacing="2" class="TOCbug" align="right"><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></table> <a name="rfc.section.3"></a><h3>3. Extension Namespace</h3> <p> All OpenID 2.0 messages that contain a UI Extension element MUST contain the following extension namespace declaration, as specified in the - Extensions section of <a class="info" href="#OpenID%202.0">[OpenID 2.0]<span> (</span><span class="info">OpenID 2.0 Workgroup, “OpenID 2.0,” .</span><span>)</span></a>. + Extensions section of <a class='info' href='#OpenID 2.0'>[OpenID 2.0]<span> (</span><span class='info'>OpenID 2.0 Workgroup, “OpenID 2.0,” .</span><span>)</span></a>. -</p><div style="display: table; width: 0pt; margin-left: 3em; margin-right: auto;"><pre> openid.ns.<alias>=http://specs.openid.net/extensions/ui/1.0 +</p><div style='display: table; width: 0; margin-left: 3em; margin-right: auto'><pre> + openid.ns.<alias>=http://specs.openid.net/extensions/ui/1.0 </pre></div> <p> The actual extension namespace alias should be determined on a per-message basis @@ -250,8 +256,8 @@ Extension Namespace</h3> for all examples is "ui". </p> -<a name="anchor3"></a><br><hr> -<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table> +<a name="anchor3"></a><br /><hr /> +<table summary="layout" cellpadding="0" cellspacing="2" class="TOCbug" align="right"><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></table> <a name="rfc.section.4"></a><h3>4. Language Preference</h3> @@ -270,7 +276,7 @@ Language Preference</h3> </dd> <dt>openid.ui.lang</dt> <dd> - REQUIRED. The user's preferred languages as a <a class="info" href="#BCP%2047">[BCP 47]<span> (</span><span class="info">Phillips, A. and M. Davis, “Tags for Identifying Languages,” .</span><span>)</span></a> language priority list, + REQUIRED. The user's preferred languages as a <a class='info' href='#BCP 47'>[BCP 47]<span> (</span><span class='info'>Phillips, A. and M. Davis, “Tags for Identifying Languages,” .</span><span>)</span></a> language priority list, represented as a comma-separated list of BCP 47 basic language ranges in descending priority order. For instance, the value "fr-CA,fr-FR,en-CA" represents the preference for French spoken in Canada, French spoken in France, followed by English spoken in Canada. @@ -285,8 +291,8 @@ Language Preference</h3> preference inferred by the user's IP address. </p> -<a name="anchor4"></a><br><hr> -<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table> +<a name="anchor4"></a><br /><hr /> +<table summary="layout" cellpadding="0" cellspacing="2" class="TOCbug" align="right"><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></table> <a name="rfc.section.5"></a><h3>5. Requesting Authentication in a Popup</h3> @@ -312,7 +318,11 @@ Requesting Authentication in a Popup</h3> <dt>openid.ui.mode</dt> <dd> REQUIRED. Value: "popup". - New modes may be defined in future versions of this extension. + New modes may be defined in future versions of this extension. Any mode starting with the prefix "x-" should be + considered experimental. If an OpenID provider receives a request containing an experimental mode, and it does + not recognize that mode, it SHOULD NOT throw an error or invalidate further processing of this extension. If no other + parameters are present, then the OpenID provider receiving an experimental mode SHOULD continue processing the OpenID + request as if this extension were not included in it. </dd> </dl></blockquote><p> @@ -347,13 +357,13 @@ Requesting Authentication in a Popup</h3> </p> <p> The response to an authentication request in a popup is unchanged from - <a class="info" href="#OpenID%202.0">[OpenID 2.0]<span> (</span><span class="info">OpenID 2.0 Workgroup, “OpenID 2.0,” .</span><span>)</span></a>. + <a class='info' href='#OpenID 2.0'>[OpenID 2.0]<span> (</span><span class='info'>OpenID 2.0 Workgroup, “OpenID 2.0,” .</span><span>)</span></a>. Relying Parties detecting that the popup was closed without receiving an authentication response SHOULD interpret the close event to be a negative assertion. </p> -<a name="anchor5"></a><br><hr> -<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table> +<a name="anchor5"></a><br /><hr /> +<table summary="layout" cellpadding="0" cellspacing="2" class="TOCbug" align="right"><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></table> <a name="rfc.section.5.1"></a><h3>5.1. Authentication Response in a Fragment</h3> @@ -365,17 +375,66 @@ Authentication Response in a Fragment</h3> URL in the authentication request. If the fragment delimiter character is present in the return_to URL, the OpenID Provider SHOULD return the response parameters in the fragment portion of the URL. If the return_to URL already contains a question mark "?", the first response parameter MUST be prefixed - with an ampersand "&", otherwise the first response parameter MUST be prefixed with a question mark "?". + with an ampersand "&", otherwise the first response parameter MUST be prefixed with a question mark "?". </p> -<a name="anchor6"></a><br><hr> -<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table> +<a name="anchor6"></a><br /><hr /> +<table summary="layout" cellpadding="0" cellspacing="2" class="TOCbug" align="right"><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></table> <a name="rfc.section.6"></a><h3>6. +Requesting Display of RP icons in the OP Approval UI</h3> + +<p> + When requesting authentication, the Relying Party MAY indicate to the OpenID Provider + the availability of graphical resources to represent the Relying Party brand at the OpenID Provider's approval UI. + This is indicated by including the following parameter: + </p> +<blockquote class="text"><dl> +<dt>openid.ui.icon</dt> +<dd> + REQUIRED. Value: "true" + +</dd> +</dl></blockquote><p> + In order to retrieve the indicated graphical resources, the OpenID Provider performs discovery on the Relying Party, as specified + in <a class='info' href='#OpenID 2.0'>[OpenID 2.0]<span> (</span><span class='info'>OpenID 2.0 Workgroup, “OpenID 2.0,” .</span><span>)</span></a> (or future versions of the OpenID protocol specification). + The RP SHOULD indicate the location of the graphical resource by adding an entry to its XRDS document: + +</p><div style='display: table; width: 0; margin-left: 3em; margin-right: auto'><pre> + +<Service xmlns="xri://$xrd*($v*2.0)"> + <Type>http://specs.openid.net/extensions/ui/icon</Type> + <URI>http://consumer.example.com/images/image.jpg</URI> +</Service> + +<Service xmlns="xri://$xrd*($v*2.0)"> + <Type>http://specs.openid.net/extensions/ui/icon</Type> + <URI>http://consumer.example.com/favicon.ico</URI> +</Service> + +</pre></div> +<p> + If the Relying Party indicates availability of graphical resources using the "icon" parameter but the OpenID Provider + does not succeed in obtaining a discovery document at the Relying Party, the OpenID Provider MAY attempt to locate a graphical + resource at the domain indicated by "openid.realm", under the path "/favicon.ico". If the realm contains the wildcard "*" for the host, + the OpenID Provider should replace it with "www". + In this case, the OpenID provider MAY restrict + the display of the resource to 16x16 format, and the Relying Party SHOULD ensure that the resource displays well in 16x16 format. + +</p> +<p> + It is RECOMMENDED that the OpenID Provider do not inline graphical resources from the Relying Party without verification. Instead, + the OpenID Provider SHOULD proxy the icons after performing appropriate sanitization. Proxying is also necessary to avoid mixed-content + warnings if the OpenID Provider approval page is served over HTTPS but the graphical resource is only available over HTTP. + +</p> +<a name="anchor7"></a><br /><hr /> +<table summary="layout" cellpadding="0" cellspacing="2" class="TOCbug" align="right"><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></table> +<a name="rfc.section.7"></a><h3>7. Discovery</h3> <p> OpenID Providers supporting the User Interface Extension SHOULD advertise their support of the - Extension using OpenID Discovery as defined in Section 7.3 of <a class="info" href="#OpenID%202.0">[OpenID 2.0]<span> (</span><span class="info">OpenID 2.0 Workgroup, “OpenID 2.0,” .</span><span>)</span></a>. + Extension using OpenID Discovery as defined in Section 7.3 of <a class='info' href='#OpenID 2.0'>[OpenID 2.0]<span> (</span><span class='info'>OpenID 2.0 Workgroup, “OpenID 2.0,” .</span><span>)</span></a>. </p> <p> @@ -383,7 +442,8 @@ Discovery</h3> http://specs.openid.net/extensions/ui/1.0/lang-pref as a <xrd:Type> child element of the <xrd:Service> element in the XRDS discovery document. -</p><div style="display: table; width: 0pt; margin-left: 3em; margin-right: auto;"><pre><Type>http://specs.openid.net/extensions/ui/1.0/lang-pref</Type> +</p><div style='display: table; width: 0; margin-left: 3em; margin-right: auto'><pre> +<Type>http://specs.openid.net/extensions/ui/1.0/lang-pref</Type> </pre></div> <p> OpenID Providers supporting the popup functionality SHOULD define @@ -391,11 +451,20 @@ Discovery</h3> of the <xrd:Service> element in the XRDS discovery document. -</p><div style="display: table; width: 0pt; margin-left: 3em; margin-right: auto;"><pre><Type>http://specs.openid.net/extensions/ui/1.0/mode/popup</Type> +</p><div style='display: table; width: 0; margin-left: 3em; margin-right: auto'><pre> +<Type>http://specs.openid.net/extensions/ui/1.0/mode/popup</Type> </pre></div> -<a name="anchor7"></a><br><hr> -<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table> -<a name="rfc.section.7"></a><h3>7. +<p> + OpenID Providers supporting the graphical RP representation functionality SHOULD define + http://specs.openid.net/extensions/ui/1.0/icon as a <xrd:Type> child element of the <xrd:Service> element + in the XRDS discovery document. + +</p><div style='display: table; width: 0; margin-left: 3em; margin-right: auto'><pre> +<Type>http://specs.openid.net/extensions/ui/1.0/icon</Type> +</pre></div> +<a name="anchor8"></a><br /><hr /> +<table summary="layout" cellpadding="0" cellspacing="2" class="TOCbug" align="right"><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></table> +<a name="rfc.section.8"></a><h3>8. Considerations</h3> <p> @@ -410,9 +479,9 @@ Considerations</h3> window to protect the user's credentials and approval from clickjacking exploits. </p> -<a name="anchor8"></a><br><hr> -<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table> -<a name="rfc.section.8"></a><h3>8. +<a name="anchor9"></a><br /><hr /> +<table summary="layout" cellpadding="0" cellspacing="2" class="TOCbug" align="right"><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></table> +<a name="rfc.section.9"></a><h3>9. Acknowledgements</h3> <p> @@ -435,27 +504,89 @@ Acknowledgements</h3> David Recordon (david@sixapart.com) </p> -<a name="rfc.references1"></a><br><hr> -<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table> -<h3>9. References</h3> -<table border="0" width="99%"> -<tbody><tr><td class="author-text" valign="top"><a name="BCP 47">[BCP 47]</a></td> -<td class="author-text">Phillips, A. and M. Davis, “Tags for Identifying Languages,” BCP 47.</td></tr> +<a name="rfc.references1"></a><br /><hr /> +<table summary="layout" cellpadding="0" cellspacing="2" class="TOCbug" align="right"><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></table> +<h3>10. References</h3> +<table width="99%" border="0"> +<tr><td class="author-text" valign="top"><a name="BCP 47">[BCP 47]</a></td> +<td class="author-text">Phillips, A. and M. Davis, “Tags for Identifying Languages,” BCP 47.</td></tr> <tr><td class="author-text" valign="top"><a name="Language Preference Attribute">[Language Preference Attribute]</a></td> -<td class="author-text">axschema.org, “<a href="http://axschema.org/pref/language">Language Preference Attribute</a>.”</td></tr> +<td class="author-text">axschema.org, “<a href="http://axschema.org/pref/language">Language Preference Attribute</a>.”</td></tr> <tr><td class="author-text" valign="top"><a name="OpenID 2.0">[OpenID 2.0]</a></td> -<td class="author-text">OpenID 2.0 Workgroup, “<a href="http://openid.net/">OpenID 2.0</a>.”</td></tr> +<td class="author-text">OpenID 2.0 Workgroup, “<a href="http://openid.net">OpenID 2.0</a>.”</td></tr> <tr><td class="author-text" valign="top"><a name="OpenID Attribute Exchange">[OpenID Attribute Exchange]</a></td> -<td class="author-text">Hardt, D., Bufu, J., and J. Hoyt, “<a href="http://openid.net/specs/openid-attribute-exchange-1_0.html">OpenID Attribute Exchange 1.0</a>.”</td></tr> +<td class="author-text">Hardt, D., Bufu, J., and J. Hoyt, “<a href="http://openid.net/specs/openid-attribute-exchange-1_0.html">OpenID Attribute Exchange 1.0</a>.”</td></tr> <tr><td class="author-text" valign="top"><a name="RFC2119">[RFC2119]</a></td> -<td class="author-text">Bradner, B., “<a href="http://tools.ietf.org/html/rfc2119">Key words for use in RFCs to Indicate Requirement Levels</a>,” RFC 2119.</td></tr> -</tbody></table> +<td class="author-text">Bradner, B., “<a href="http://tools.ietf.org/html/rfc2119">Key words for use in RFCs to Indicate Requirement Levels</a>,” RFC 2119.</td></tr> +</table> -<a name="rfc.authors"></a><br><hr> -<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table> +<a name="anchor11"></a><br /><hr /> +<table summary="layout" cellpadding="0" cellspacing="2" class="TOCbug" align="right"><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></table> +<a name="rfc.section.A"></a><h3>Appendix A. +Example Use of Experimental Mode</h3> + +<p> + In OpenID authentication, when using the checkid_immediate mode, there is no mechanism to indicate that there is a user logged in at the OpenID Provider. + Therefore, the Relying Party does not know if the checkid_immediate request failed because: + </p> +<ol class="text"> +<li>The user does not have an account at the OpenID Provider (or is not logged in at the Provider), or: +</li> +<li>The user is logged in to the OpenID Provider but has not yet approved transparent login with the Relying Party. +</li> +</ol><p> + This makes it difficult for the RP to optimize the OpenID + user experience by, for instance, displaying a prominent button for the OpenID Provider in case (2). The following example shows how an experimental mode can be sent with + checkid_immediate requests to obtain this information. + +</p> +<p> + </p> +<blockquote class="text"><dl> +<dt>openid.ns.ui</dt> +<dd> + REQUIRED. Value: "http://specs.openid.net/extensions/ui/1.0" + +</dd> +<dt>openid.ui.mode</dt> +<dd> + REQUIRED. Value: "x-has-session". + +</dd> +</dl></blockquote><p> + +</p> +<p> + To respond, the OpenID provider sends identical parameters in the "setup_needed" response to answer affirmatively (i.e., there IS an authenticated browser session): + </p> +<blockquote class="text"><dl> +<dt>openid.ns.ui</dt> +<dd> + REQUIRED. Value: "http://specs.openid.net/extensions/ui/1.0" + +</dd> +<dt>openid.ui.mode</dt> +<dd> + REQUIRED. Value: "x-has-session". + +</dd> +</dl></blockquote><p> + Alternative, if the OpenID provider needs to indicate the LACK of a session, it sends simply the UI namespace, without a mode, in the "setup_needed" response: + </p> +<blockquote class="text"><dl> +<dt>openid.ns.ui</dt> +<dd> + REQUIRED. Value: "http://specs.openid.net/extensions/ui/1.0" + +</dd> +</dl></blockquote><p> + +</p> +<a name="rfc.authors"></a><br /><hr /> +<table summary="layout" cellpadding="0" cellspacing="2" class="TOCbug" align="right"><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></table> <h3>Authors' Addresses</h3> -<table border="0" cellpadding="0" cellspacing="0" width="99%"> -<tbody><tr><td class="author-text"> </td> +<table width="99%" border="0" cellpadding="0" cellspacing="0"> +<tr><td class="author-text"> </td> <td class="author-text">Allen Tom</td></tr> <tr><td class="author-text"> </td> <td class="author-text">Yahoo!</td></tr> @@ -468,5 +599,5 @@ Acknowledgements</h3> <td class="author-text">Google</td></tr> <tr><td class="author" align="right">Email: </td> <td class="author-text"><a href="mailto:breno@google.com">breno@google.com</a></td></tr> -</tbody></table> -</body></html>
\ No newline at end of file +</table> +</body></html> diff --git a/lib/Microsoft.Contracts.dll b/lib/Microsoft.Contracts.dll Binary files differindex 012c506..fade5e1 100644 --- a/lib/Microsoft.Contracts.dll +++ b/lib/Microsoft.Contracts.dll diff --git a/projecttemplates/MvcRelyingParty.vsixmanifest b/projecttemplates/MvcRelyingParty.vsixmanifest index f401277..b14c99e 100644 --- a/projecttemplates/MvcRelyingParty.vsixmanifest +++ b/projecttemplates/MvcRelyingParty.vsixmanifest @@ -1,7 +1,7 @@ <?xml version="1.0"?> <Vsix xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Version="1.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2010"> <Identifier Id="DotNetOpenAuth.MvcRelyingParty.15E8EC96-BDC3-47E2-8BB1-0483E9D16705"> - <Name>ASP.NET MVC OpenID web site (C#)</Name> + <Name>ASP.NET MVC 2 OpenID web site (C#)</Name> <Author>DotNetOpenAuth</Author> <Version>$version$</Version> <Description>Resources for developing applications that use OpenID, OAuth, and InfoCard.</Description> @@ -13,12 +13,12 @@ <InstalledByMsi>false</InstalledByMsi> <SupportedProducts> <VisualStudio Version="10.0"> - <Edition>VSTS</Edition> - <Edition>VSTD</Edition> <Edition>Pro</Edition> + <Edition>Premium</Edition> + <Edition>Ultimate</Edition> </VisualStudio> </SupportedProducts> - <SupportedFrameworkRuntimeEdition MinVersion="3.5" MaxVersion="3.5" /> + <SupportedFrameworkRuntimeEdition MinVersion="3.5" MaxVersion="4.0" /> </Identifier> <References /> <Content> diff --git a/projecttemplates/MvcRelyingParty/MvcRelyingParty.csproj b/projecttemplates/MvcRelyingParty/MvcRelyingParty.csproj index 7097e07..540b702 100644 --- a/projecttemplates/MvcRelyingParty/MvcRelyingParty.csproj +++ b/projecttemplates/MvcRelyingParty/MvcRelyingParty.csproj @@ -52,13 +52,13 @@ <Reference Include="System.Data.Entity"> <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> - <Reference Include="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" /> + <Reference Include="System.Web.Mvc" /> <Reference Include="System.Xml.Linq"> <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> <Reference Include="System.Drawing" /> <Reference Include="System.Web" /> - <Reference Include="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" /> + <Reference Include="System.Web.Extensions" /> <Reference Include="System.Web.Abstractions" /> <Reference Include="System.Web.Routing" /> <Reference Include="System.Xml" /> diff --git a/projecttemplates/WebFormsRelyingParty.vsixmanifest b/projecttemplates/WebFormsRelyingParty.vsixmanifest index 75a3646..6ebe392 100644 --- a/projecttemplates/WebFormsRelyingParty.vsixmanifest +++ b/projecttemplates/WebFormsRelyingParty.vsixmanifest @@ -13,12 +13,12 @@ <InstalledByMsi>false</InstalledByMsi> <SupportedProducts> <VisualStudio Version="10.0"> - <Edition>VSTS</Edition> - <Edition>VSTD</Edition> <Edition>Pro</Edition> + <Edition>Premium</Edition> + <Edition>Ultimate</Edition> </VisualStudio> </SupportedProducts> - <SupportedFrameworkRuntimeEdition MinVersion="3.5" MaxVersion="3.5" /> + <SupportedFrameworkRuntimeEdition MinVersion="3.5" MaxVersion="4.0" /> </Identifier> <References /> <Content> diff --git a/projecttemplates/projecttemplates.proj b/projecttemplates/projecttemplates.proj index 0602362..470d1f3 100644 --- a/projecttemplates/projecttemplates.proj +++ b/projecttemplates/projecttemplates.proj @@ -209,7 +209,7 @@ <DowngradeProjects Projects="@(ProjectTemplates2008Layout)" Condition="'%(Extension)' == '.csproj'" - DowngradeMvc2ToMvc1="true" + DowngradeMvc2ToMvc1="$(DowngradeMvc2ToMvc1)" /> <Purge Directories="$(ProjectTemplates2008LayoutPath)" diff --git a/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj b/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj index eab27db..41aff68 100644 --- a/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj +++ b/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj @@ -88,6 +88,7 @@ <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="TwitterConsumer.cs" /> <Compile Include="Util.cs" /> + <Compile Include="YubikeyRelyingParty.cs" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\..\src\DotNetOpenAuth\DotNetOpenAuth.csproj"> diff --git a/samples/DotNetOpenAuth.ApplicationBlock/InMemoryTokenManager.cs b/samples/DotNetOpenAuth.ApplicationBlock/InMemoryTokenManager.cs index e83817a..8b769de 100644 --- a/samples/DotNetOpenAuth.ApplicationBlock/InMemoryTokenManager.cs +++ b/samples/DotNetOpenAuth.ApplicationBlock/InMemoryTokenManager.cs @@ -10,6 +10,7 @@ namespace DotNetOpenAuth.ApplicationBlock { using System.Diagnostics; using DotNetOpenAuth.OAuth.ChannelElements; using DotNetOpenAuth.OAuth.Messages; + using DotNetOpenAuth.OpenId.Extensions.OAuth; /// <summary> /// A token manager that only retains tokens in memory. @@ -20,7 +21,7 @@ namespace DotNetOpenAuth.ApplicationBlock { /// where the user only signs in without providing any authorization to access /// Twitter APIs except to authenticate, since that access token is only useful once. /// </remarks> - public class InMemoryTokenManager : IConsumerTokenManager { + public class InMemoryTokenManager : IConsumerTokenManager, IOpenIdOAuthTokenManager { private Dictionary<string, string> tokensAndSecrets = new Dictionary<string, string>(); /// <summary> @@ -118,5 +119,25 @@ namespace DotNetOpenAuth.ApplicationBlock { } #endregion + + #region IOpenIdOAuthTokenManager Members + + /// <summary> + /// Stores a new request token obtained over an OpenID request. + /// </summary> + /// <param name="consumerKey">The consumer key.</param> + /// <param name="authorization">The authorization message carrying the request token and authorized access scope.</param> + /// <remarks> + /// <para>The token secret is the empty string.</para> + /// <para>Tokens stored by this method should be short-lived to mitigate + /// possible security threats. Their lifetime should be sufficient for the + /// relying party to receive the positive authentication assertion and immediately + /// send a follow-up request for the access token.</para> + /// </remarks> + public void StoreOpenIdAuthorizedRequestToken(string consumerKey, AuthorizationApprovedResponse authorization) { + this.tokensAndSecrets[authorization.RequestToken] = String.Empty; + } + + #endregion } }
\ No newline at end of file diff --git a/samples/DotNetOpenAuth.ApplicationBlock/YubikeyRelyingParty.cs b/samples/DotNetOpenAuth.ApplicationBlock/YubikeyRelyingParty.cs new file mode 100644 index 0000000..6be749e --- /dev/null +++ b/samples/DotNetOpenAuth.ApplicationBlock/YubikeyRelyingParty.cs @@ -0,0 +1,207 @@ +//----------------------------------------------------------------------- +// <copyright file="YubikeyRelyingParty.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock { + using System; + using System.Collections.Specialized; + using System.Globalization; + using System.IO; + using System.Net; + using System.Text; + using System.Text.RegularExpressions; + + /// <summary> + /// The set of possible results from verifying a Yubikey token. + /// </summary> + public enum YubikeyResult { + /// <summary> + /// The OTP is valid. + /// </summary> + Ok, + + /// <summary> + /// The OTP is invalid format. + /// </summary> + BadOtp, + + /// <summary> + /// The OTP has already been seen by the service. + /// </summary> + ReplayedOtp, + + /// <summary> + /// The HMAC signature verification failed. + /// </summary> + /// <remarks> + /// This indicates a bug in the relying party code. + /// </remarks> + BadSignature, + + /// <summary> + /// The request lacks a parameter. + /// </summary> + /// <remarks> + /// This indicates a bug in the relying party code. + /// </remarks> + MissingParameter, + + /// <summary> + /// The request id does not exist. + /// </summary> + NoSuchClient, + + /// <summary> + /// The request id is not allowed to verify OTPs. + /// </summary> + OperationNotAllowed, + + /// <summary> + /// Unexpected error in our server. Please contact Yubico if you see this error. + /// </summary> + BackendError, + } + + /// <summary> + /// Provides verification of a Yubikey one-time password (OTP) as a means of authenticating + /// a user at your web site or application. + /// </summary> + /// <remarks> + /// Please visit http://yubico.com/ for more information about this authentication method. + /// </remarks> + public class YubikeyRelyingParty { + /// <summary> + /// The default Yubico authorization server to use for validation and replay protection. + /// </summary> + private const string DefaultYubicoAuthorizationServer = "https://api.yubico.com/wsapi/verify"; + + /// <summary> + /// The format of the lines in the Yubico server response. + /// </summary> + private static readonly Regex ResultLineMatcher = new Regex(@"^(?<key>[^=]+)=(?<value>.*)$"); + + /// <summary> + /// The Yubico authorization server to use for validation and replay protection. + /// </summary> + private readonly string yubicoAuthorizationServer; + + /// <summary> + /// The authorization ID assigned to your individual site by Yubico. + /// </summary> + private readonly int yubicoAuthorizationId; + + /// <summary> + /// Initializes a new instance of the <see cref="YubikeyRelyingParty"/> class + /// that uses the default Yubico server for validation and replay protection. + /// </summary> + /// <param name="authorizationId">The authorization ID assigned to your individual site by Yubico. + /// Get one from https://upgrade.yubico.com/getapikey/</param> + public YubikeyRelyingParty(int authorizationId) + : this(authorizationId, DefaultYubicoAuthorizationServer) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="YubikeyRelyingParty"/> class. + /// </summary> + /// <param name="authorizationId">The authorization ID assigned to your individual site by Yubico. + /// Contact tech@yubico.com if you haven't got an authId for your site.</param> + /// <param name="yubicoAuthorizationServer">The Yubico authorization server to use for validation and replay protection.</param> + public YubikeyRelyingParty(int authorizationId, string yubicoAuthorizationServer) { + if (authorizationId < 0) { + throw new ArgumentOutOfRangeException("authorizationId"); + } + + if (!Uri.IsWellFormedUriString(yubicoAuthorizationServer, UriKind.Absolute)) { + throw new ArgumentException("Invalid authorization server URI", "yubicoAuthorizationServer"); + } + + if (!yubicoAuthorizationServer.StartsWith(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { + throw new ArgumentException("HTTPS is required for the Yubico server. HMAC response verification not supported.", "yubicoAuthorizationServer"); + } + + this.yubicoAuthorizationId = authorizationId; + this.yubicoAuthorizationServer = yubicoAuthorizationServer; + } + + /// <summary> + /// Extracts the username out of a Yubikey token. + /// </summary> + /// <param name="yubikeyToken">The yubikey token.</param> + /// <returns>A 12 character string that is unique for this particular Yubikey device.</returns> + public static string ExtractUsername(string yubikeyToken) { + EnsureWellFormedToken(yubikeyToken); + return yubikeyToken.Substring(0, 12); + } + + /// <summary> + /// Determines whether the specified yubikey token is valid and has not yet been used. + /// </summary> + /// <param name="yubikeyToken">The yubikey token.</param> + /// <returns> + /// <c>true</c> if the specified yubikey token is valid; otherwise, <c>false</c>. + /// </returns> + /// <exception cref="WebException">Thrown when the validity of the token could not be confirmed due to network issues.</exception> + public YubikeyResult IsValid(string yubikeyToken) { + EnsureWellFormedToken(yubikeyToken); + + StringBuilder authorizationUri = new StringBuilder(this.yubicoAuthorizationServer); + authorizationUri.Append("?id="); + authorizationUri.Append(Uri.EscapeDataString(this.yubicoAuthorizationId.ToString(CultureInfo.InvariantCulture))); + authorizationUri.Append("&otp="); + authorizationUri.Append(Uri.EscapeDataString(yubikeyToken)); + + var request = WebRequest.Create(authorizationUri.ToString()); + using (var response = request.GetResponse()) { + using (var responseReader = new StreamReader(response.GetResponseStream())) { + string line; + var result = new NameValueCollection(); + while ((line = responseReader.ReadLine()) != null) { + Match m = ResultLineMatcher.Match(line); + if (m.Success) { + result[m.Groups["key"].Value] = m.Groups["value"].Value; + } + } + + return ParseResult(result["status"]); + } + } + } + + /// <summary> + /// Parses the Yubico server result. + /// </summary> + /// <param name="status">The status field from the response.</param> + /// <returns>The enum value representing the result.</returns> + private static YubikeyResult ParseResult(string status) { + switch (status) { + case "OK": return YubikeyResult.Ok; + case "BAD_OTP": return YubikeyResult.BadOtp; + case "REPLAYED_OTP": return YubikeyResult.ReplayedOtp; + case "BAD_SIGNATURE": return YubikeyResult.BadSignature; + case "MISSING_PARAMETER": return YubikeyResult.MissingParameter; + case "NO_SUCH_CLIENT": return YubikeyResult.NoSuchClient; + case "OPERATION_NOT_ALLOWED": return YubikeyResult.OperationNotAllowed; + case "BACKEND_ERROR": return YubikeyResult.BackendError; + default: throw new ArgumentOutOfRangeException("status", status, "Unexpected status value."); + } + } + + /// <summary> + /// Ensures the OTP is well formed. + /// </summary> + /// <param name="yubikeyToken">The yubikey token.</param> + private static void EnsureWellFormedToken(string yubikeyToken) { + if (yubikeyToken == null) { + throw new ArgumentNullException("yubikeyToken"); + } + + yubikeyToken = yubikeyToken.Trim(); + + if (yubikeyToken.Length <= 12) { + throw new ArgumentException("Yubikey token has unexpected length."); + } + } + } +} diff --git a/samples/OpenIdOfflineProvider/MainWindow.xaml b/samples/OpenIdOfflineProvider/MainWindow.xaml index de215ba..5e9438f 100644 --- a/samples/OpenIdOfflineProvider/MainWindow.xaml +++ b/samples/OpenIdOfflineProvider/MainWindow.xaml @@ -6,7 +6,9 @@ <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> <RowDefinition Height="*"/> + <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="auto" /> @@ -20,6 +22,13 @@ <ComboBoxItem>Auto respond: No</ComboBoxItem> <ComboBoxItem>Intercept</ComboBoxItem> </ComboBox> - <TextBox Height="auto" Margin="0,8,0,0" Grid.Row="2" Grid.ColumnSpan="2" Name="logBox" IsReadOnly="True" VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Auto" /> + <Expander Grid.Row="2" Grid.ColumnSpan="2" Header="Advanced options"> + <StackPanel Margin="3"> + <CheckBox Name="directedIdentityTrailingPeriodsCheckbox">Directed identity uses trailing periods in path</CheckBox> + <CheckBox Name="capitalizedHostName">Directed identity uses capitalized host name in claimed identifier</CheckBox> + </StackPanel> + </Expander> + <TextBox Height="auto" Margin="0,8,0,0" Grid.Row="3" Grid.ColumnSpan="2" Name="logBox" IsReadOnly="True" VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Auto" /> + <Button Grid.Row="4" Grid.ColumnSpan="2" Name="clearLogButton" Click="ClearLogButton_Click">Clear log</Button> </Grid> </Window> diff --git a/samples/OpenIdOfflineProvider/MainWindow.xaml.cs b/samples/OpenIdOfflineProvider/MainWindow.xaml.cs index 8f04da3..0fcac9c 100644 --- a/samples/OpenIdOfflineProvider/MainWindow.xaml.cs +++ b/samples/OpenIdOfflineProvider/MainWindow.xaml.cs @@ -25,6 +25,7 @@ namespace DotNetOpenAuth.OpenIdOfflineProvider { using System.Windows.Navigation; using System.Windows.Shapes; using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId; using DotNetOpenAuth.OpenId.Provider; using log4net; using log4net.Appender; @@ -117,7 +118,15 @@ namespace DotNetOpenAuth.OpenIdOfflineProvider { switch (checkidRequestList.SelectedIndex) { case 0: if (authRequest.IsDirectedIdentity) { - authRequest.ClaimedIdentifier = new Uri(this.hostedProvider.UserIdentityPageBase, "directedidentity"); + string userIdentityPageBase = this.hostedProvider.UserIdentityPageBase.AbsoluteUri; + if (capitalizedHostName.IsChecked.Value) { + userIdentityPageBase = (this.hostedProvider.UserIdentityPageBase.Scheme + Uri.SchemeDelimiter + this.hostedProvider.UserIdentityPageBase.Authority).ToUpperInvariant() + this.hostedProvider.UserIdentityPageBase.PathAndQuery; + } + string leafPath = "directedidentity"; + if (directedIdentityTrailingPeriodsCheckbox.IsChecked.Value) { + leafPath += "."; + } + authRequest.ClaimedIdentifier = Identifier.Parse(userIdentityPageBase + leafPath, true); authRequest.LocalIdentifier = authRequest.ClaimedIdentifier; } authRequest.IsAuthenticated = true; @@ -169,5 +178,14 @@ namespace DotNetOpenAuth.OpenIdOfflineProvider { MessageBox.Show(this, ex.Message, "Error while copying OP Identifier to the clipboard", MessageBoxButton.OK, MessageBoxImage.Error); } } + + /// <summary> + /// Handles the Click event of the ClearLogButton control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param> + private void ClearLogButton_Click(object sender, RoutedEventArgs e) { + logBox.Clear(); + } } } diff --git a/samples/OpenIdProviderMvc/OpenIdProviderMvc.csproj b/samples/OpenIdProviderMvc/OpenIdProviderMvc.csproj index a7a2296..72b6c14 100644 --- a/samples/OpenIdProviderMvc/OpenIdProviderMvc.csproj +++ b/samples/OpenIdProviderMvc/OpenIdProviderMvc.csproj @@ -44,19 +44,13 @@ <Reference Include="System.ComponentModel.DataAnnotations"> <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> - <Reference Include="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> - <HintPath>..\..\lib\System.Web.Mvc.dll</HintPath> - </Reference> + <Reference Include="System.Web.Mvc" /> <Reference Include="System.Web" /> <Reference Include="System.Web.Extensions"> <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> - <Reference Include="System.Web.Abstractions"> - <HintPath>..\..\lib\System.Web.Abstractions.dll</HintPath> - </Reference> - <Reference Include="System.Web.Routing"> - <HintPath>..\..\lib\System.Web.Routing.dll</HintPath> - </Reference> + <Reference Include="System.Web.Abstractions" /> + <Reference Include="System.Web.Routing" /> <Reference Include="System.Xml" /> <Reference Include="System.Configuration" /> <Reference Include="System.Web.Services" /> diff --git a/samples/OpenIdProviderMvc/Web.config b/samples/OpenIdProviderMvc/Web.config index 8f145b0..f8d2389 100644 --- a/samples/OpenIdProviderMvc/Web.config +++ b/samples/OpenIdProviderMvc/Web.config @@ -194,12 +194,12 @@ <runtime> <legacyHMACWarning enabled="0" /> - <!-- If you target ASP.NET MVC 2, uncomment this so that MVC 1 components such as DotNetOpenAuth will work with it. + <!-- If you target ASP.NET MVC 2, uncomment this so that MVC 1 components such as DotNetOpenAuth will work with it. --> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0" /> </dependentAssembly> - </assemblyBinding>--> + </assemblyBinding> </runtime> </configuration> diff --git a/samples/OpenIdProviderWebForms/OpenIdProviderWebForms.csproj b/samples/OpenIdProviderWebForms/OpenIdProviderWebForms.csproj index 603b372..4cd84c1 100644 --- a/samples/OpenIdProviderWebForms/OpenIdProviderWebForms.csproj +++ b/samples/OpenIdProviderWebForms/OpenIdProviderWebForms.csproj @@ -183,6 +183,10 @@ <Project>{3191B653-F76D-4C1A-9A5A-347BC3AAAAB7}</Project> <Name>DotNetOpenAuth</Name> </ProjectReference> + <ProjectReference Include="..\DotNetOpenAuth.ApplicationBlock\DotNetOpenAuth.ApplicationBlock.csproj"> + <Project>{AA78D112-D889-414B-A7D4-467B34C7B663}</Project> + <Name>DotNetOpenAuth.ApplicationBlock</Name> + </ProjectReference> </ItemGroup> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" /> diff --git a/samples/OpenIdProviderWebForms/Web.config b/samples/OpenIdProviderWebForms/Web.config index b76231b..de92621 100644 --- a/samples/OpenIdProviderWebForms/Web.config +++ b/samples/OpenIdProviderWebForms/Web.config @@ -62,6 +62,11 @@ <reporting enabled="true" /> </dotNetOpenAuth> + <appSettings> + <!-- Get your own Yubico API key here: https://upgrade.yubico.com/getapikey/ --> + <add key="YubicoAPIKey" value="3961"/> + </appSettings> + <system.web> <!-- Set compilation debug="true" to insert debugging diff --git a/samples/OpenIdProviderWebForms/decide.aspx.cs b/samples/OpenIdProviderWebForms/decide.aspx.cs index b392d85..40c17c0 100644 --- a/samples/OpenIdProviderWebForms/decide.aspx.cs +++ b/samples/OpenIdProviderWebForms/decide.aspx.cs @@ -50,7 +50,12 @@ namespace OpenIdProviderWebForms { this.profileFields.SetRequiredFieldsFromRequest(requestedFields); if (!IsPostBack) { var sregResponse = requestedFields.CreateResponse(); - sregResponse.Email = Membership.GetUser().Email; + + // We MAY not have an entry for this user if they used Yubikey to log in. + MembershipUser user = Membership.GetUser(); + if (user != null) { + sregResponse.Email = Membership.GetUser().Email; + } this.profileFields.SetOpenIdProfileFields(sregResponse); } } diff --git a/samples/OpenIdProviderWebForms/login.aspx b/samples/OpenIdProviderWebForms/login.aspx index e8f42c5..f7898cc 100644 --- a/samples/OpenIdProviderWebForms/login.aspx +++ b/samples/OpenIdProviderWebForms/login.aspx @@ -14,4 +14,14 @@ <tr><td>bob3</td><td>test</td></tr> <tr><td>bob4</td><td>test</td></tr> </table> + + <asp:Panel DefaultButton="yubicoButton" runat="server" style="margin-top: 25px" ID="yubicoPanel"> + Login with Yubikey: + <asp:TextBox runat="server" type="text" ID="yubicoBox" ToolTip="Click here and press your Yubikey button." + style="background-image: url(http://yubico.com/favicon.ico); background-repeat: no-repeat; background-position: 0px 1px; padding-left: 18px; width: 20em;" + MaxLength="44" AutoCompleteType="Disabled" /> + <asp:Button runat="server" ID="yubicoButton" Text="Login" + onclick="yubicoButton_Click" /> + <asp:Label Text="[Yubikey Result]" runat="server" EnableViewState="false" Visible="false" ForeColor="Red" ID="yubikeyFailureLabel" /> + </asp:Panel> </asp:Content>
\ No newline at end of file diff --git a/samples/OpenIdProviderWebForms/login.aspx.cs b/samples/OpenIdProviderWebForms/login.aspx.cs index 4051877..ef5b2c4 100644 --- a/samples/OpenIdProviderWebForms/login.aspx.cs +++ b/samples/OpenIdProviderWebForms/login.aspx.cs @@ -1,6 +1,10 @@ namespace OpenIdProviderWebForms { using System; + using System.Configuration; + using System.Globalization; + using System.Web.Security; using System.Web.UI.WebControls; + using DotNetOpenAuth.ApplicationBlock; using DotNetOpenAuth.OpenId.Provider; /// <summary> @@ -9,6 +13,8 @@ namespace OpenIdProviderWebForms { public partial class login : System.Web.UI.Page { protected void Page_Load(object src, EventArgs e) { if (!IsPostBack) { + this.yubicoPanel.Visible = !string.IsNullOrEmpty(ConfigurationManager.AppSettings["YubicoAPIKey"]); + if (ProviderEndpoint.PendingAuthenticationRequest != null && !ProviderEndpoint.PendingAuthenticationRequest.IsDirectedIdentity) { this.login1.UserName = Code.Util.ExtractUserName( @@ -18,5 +24,27 @@ namespace OpenIdProviderWebForms { } } } + + protected void yubicoButton_Click(object sender, EventArgs e) { + string username; + if (this.TryVerifyYubikeyAndGetUsername(this.yubicoBox.Text, out username)) { + FormsAuthentication.RedirectFromLoginPage(username, false); + } + } + + private bool TryVerifyYubikeyAndGetUsername(string token, out string username) { + var yubikey = new YubikeyRelyingParty(int.Parse(ConfigurationManager.AppSettings["YubicoAPIKey"], CultureInfo.InvariantCulture)); + YubikeyResult result = yubikey.IsValid(token); + switch (result) { + case YubikeyResult.Ok: + username = YubikeyRelyingParty.ExtractUsername(token); + return true; + default: + this.yubikeyFailureLabel.Visible = true; + this.yubikeyFailureLabel.Text = result.ToString(); + username = null; + return false; + } + } } }
\ No newline at end of file diff --git a/samples/OpenIdProviderWebForms/login.aspx.designer.cs b/samples/OpenIdProviderWebForms/login.aspx.designer.cs index 83826a1..307dd96 100644 --- a/samples/OpenIdProviderWebForms/login.aspx.designer.cs +++ b/samples/OpenIdProviderWebForms/login.aspx.designer.cs @@ -1,10 +1,9 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:2.0.50727.3521 // // Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. +// the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------ @@ -21,5 +20,41 @@ namespace OpenIdProviderWebForms { /// To modify move field declaration from designer file to code-behind file. /// </remarks> protected global::System.Web.UI.WebControls.Login login1; + + /// <summary> + /// yubicoPanel control. + /// </summary> + /// <remarks> + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// </remarks> + protected global::System.Web.UI.WebControls.Panel yubicoPanel; + + /// <summary> + /// yubicoBox control. + /// </summary> + /// <remarks> + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// </remarks> + protected global::System.Web.UI.WebControls.TextBox yubicoBox; + + /// <summary> + /// yubicoButton control. + /// </summary> + /// <remarks> + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// </remarks> + protected global::System.Web.UI.WebControls.Button yubicoButton; + + /// <summary> + /// yubikeyFailureLabel control. + /// </summary> + /// <remarks> + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// </remarks> + protected global::System.Web.UI.WebControls.Label yubikeyFailureLabel; } } diff --git a/samples/OpenIdRelyingPartyMvc/OpenIdRelyingPartyMvc.csproj b/samples/OpenIdRelyingPartyMvc/OpenIdRelyingPartyMvc.csproj index a94bd88..a3c7b78 100644 --- a/samples/OpenIdRelyingPartyMvc/OpenIdRelyingPartyMvc.csproj +++ b/samples/OpenIdRelyingPartyMvc/OpenIdRelyingPartyMvc.csproj @@ -43,19 +43,13 @@ <Reference Include="System.ComponentModel.DataAnnotations"> <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> - <Reference Include="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> - <HintPath>..\..\lib\System.Web.Mvc.dll</HintPath> - </Reference> + <Reference Include="System.Web.Mvc" /> <Reference Include="System.Web" /> <Reference Include="System.Web.Extensions"> <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> - <Reference Include="System.Web.Abstractions"> - <HintPath>..\..\lib\System.Web.Abstractions.dll</HintPath> - </Reference> - <Reference Include="System.Web.Routing"> - <HintPath>..\..\lib\System.Web.Routing.dll</HintPath> - </Reference> + <Reference Include="System.Web.Abstractions" /> + <Reference Include="System.Web.Routing" /> <Reference Include="System.Xml" /> <Reference Include="System.Configuration" /> <Reference Include="System.Web.Services" /> diff --git a/samples/OpenIdRelyingPartyMvc/Web.config b/samples/OpenIdRelyingPartyMvc/Web.config index 6aba949..0585cf5 100644 --- a/samples/OpenIdRelyingPartyMvc/Web.config +++ b/samples/OpenIdRelyingPartyMvc/Web.config @@ -171,12 +171,12 @@ <runtime> <legacyHMACWarning enabled="0" /> - <!-- If you target ASP.NET MVC 2, uncomment this so that MVC 1 components such as DotNetOpenAuth will work with it. + <!-- If you target ASP.NET MVC 2, uncomment this so that MVC 1 components such as DotNetOpenAuth will work with it. --> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0" /> </dependentAssembly> - </assemblyBinding>--> + </assemblyBinding> </runtime> </configuration> diff --git a/src/DotNetOpenAuth.Test/Logging.config b/src/DotNetOpenAuth.Test/Logging.config index cd19de2..87da027 100644 --- a/src/DotNetOpenAuth.Test/Logging.config +++ b/src/DotNetOpenAuth.Test/Logging.config @@ -30,7 +30,4 @@ <logger name="DotNetOpenAuth.Test"> <level value="Debug" /> </logger> - <logger name="DotNetOpenAuth.OpenId.ChannelElements.SigningBindingElement"> - <level value="WARN" /> - </logger> </log4net> diff --git a/src/DotNetOpenAuth.Test/OpenId/ChannelElements/SigningBindingElementTests.cs b/src/DotNetOpenAuth.Test/OpenId/ChannelElements/SigningBindingElementTests.cs index 8d5ef2a..6160680 100644 --- a/src/DotNetOpenAuth.Test/OpenId/ChannelElements/SigningBindingElementTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/ChannelElements/SigningBindingElementTests.cs @@ -21,7 +21,7 @@ namespace DotNetOpenAuth.Test.OpenId.ChannelElements { /// </summary> [TestCase] public void SignaturesMatchKnownGood() { - Protocol protocol = Protocol.Default; + Protocol protocol = Protocol.V20; var settings = new ProviderSecuritySettings(); var store = new AssociationMemoryStore<AssociationRelyingPartyType>(); byte[] associationSecret = Convert.FromBase64String("rsSwv1zPWfjPRQU80hciu8FPDC+GONAMJQ/AvSo1a2M="); diff --git a/src/DotNetOpenAuth.Test/OpenId/RelyingParty/PositiveAuthenticationResponseTests.cs b/src/DotNetOpenAuth.Test/OpenId/RelyingParty/PositiveAuthenticationResponseTests.cs index 811b7d1..25b0607 100644 --- a/src/DotNetOpenAuth.Test/OpenId/RelyingParty/PositiveAuthenticationResponseTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/RelyingParty/PositiveAuthenticationResponseTests.cs @@ -12,6 +12,7 @@ namespace DotNetOpenAuth.Test.OpenId.RelyingParty { using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration; using DotNetOpenAuth.OpenId.Messages; using DotNetOpenAuth.OpenId.RelyingParty; + using DotNetOpenAuth.Test.Mocks; using NUnit.Framework; [TestFixture] @@ -120,6 +121,26 @@ namespace DotNetOpenAuth.Test.OpenId.RelyingParty { Assert.IsNull(authResponse.GetCallbackArgument("a")); } + /// <summary> + /// Verifies that certain problematic claimed identifiers pass through to the RP response correctly. + /// </summary> + [TestCase] + public void ProblematicClaimedId() { + var providerEndpoint = new ProviderEndpointDescription(OpenIdTestBase.OPUri, Protocol.Default.Version); + string claimed_id = BaseMockUri + "a./b."; + var se = IdentifierDiscoveryResult.CreateForClaimedIdentifier(claimed_id, claimed_id, providerEndpoint, null, null); + UriIdentifier identityUri = (UriIdentifier)se.ClaimedIdentifier; + var mockId = new MockIdentifier(identityUri, this.MockResponder, new IdentifierDiscoveryResult[] { se }); + + var positiveAssertion = this.GetPositiveAssertion(); + positiveAssertion.ClaimedIdentifier = mockId; + positiveAssertion.LocalIdentifier = mockId; + var rp = CreateRelyingParty(); + var authResponse = new PositiveAuthenticationResponse(positiveAssertion, rp); + Assert.AreEqual(AuthenticationStatus.Authenticated, authResponse.Status); + Assert.AreEqual(claimed_id, authResponse.ClaimedIdentifier.ToString()); + } + private PositiveAssertionResponse GetPositiveAssertion() { return this.GetPositiveAssertion(false); } diff --git a/src/DotNetOpenAuth.Test/OpenId/UriIdentifierTests.cs b/src/DotNetOpenAuth.Test/OpenId/UriIdentifierTests.cs index 73c185e..5b015ff 100644 --- a/src/DotNetOpenAuth.Test/OpenId/UriIdentifierTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/UriIdentifierTests.cs @@ -112,22 +112,51 @@ namespace DotNetOpenAuth.Test.OpenId { Identifier noFragment = UriIdentifier.Parse("http://a/b"); Identifier fragment = UriIdentifier.Parse("http://a/b#c"); Assert.AreSame(noFragment, noFragment.TrimFragment()); - Assert.AreEqual(noFragment, fragment.TrimFragment()); + Assert.AreEqual(noFragment.ToString(), fragment.TrimFragment().ToString()); + + // Try the problematic ones + TestAsFullAndPartialTrust(fullTrust => { + Identifier noFrag = UriIdentifier.Parse("http://a/b./c"); + Identifier frag = UriIdentifier.Parse("http://a/b./c#d"); + Assert.AreSame(noFrag, noFrag.TrimFragment()); + Assert.AreEqual(noFrag.ToString(), frag.TrimFragment().ToString()); + }); } [TestCase] public void ToStringTest() { Assert.AreEqual(this.goodUri, new UriIdentifier(this.goodUri).ToString()); + TestAsFullAndPartialTrust(fullTrust => { + Assert.AreEqual("http://abc/D./e.?Qq#Ff", new UriIdentifier("HTTP://ABC/D./e.?Qq#Ff").ToString()); + Assert.AreEqual("http://abc/D./e.?Qq", new UriIdentifier("HTTP://ABC/D./e.?Qq").ToString()); + Assert.AreEqual("http://abc/D./e.#Ff", new UriIdentifier("HTTP://ABC/D./e.#Ff").ToString()); + Assert.AreEqual("http://abc/", new UriIdentifier("HTTP://ABC").ToString()); + Assert.AreEqual("http://abc/?q", new UriIdentifier("HTTP://ABC?q").ToString()); + Assert.AreEqual("http://abc/#f", new UriIdentifier("HTTP://ABC#f").ToString()); + }); } [TestCase] public void EqualsTest() { - Assert.AreEqual(new UriIdentifier(this.goodUri), new UriIdentifier(this.goodUri)); - // This next test is an interesting side-effect of passing off to Uri.Equals. But it's probably ok. - Assert.AreEqual(new UriIdentifier(this.goodUri), new UriIdentifier(this.goodUri + "#frag")); - Assert.AreNotEqual(new UriIdentifier(this.goodUri), new UriIdentifier(this.goodUri + "a")); - Assert.AreNotEqual(null, new UriIdentifier(this.goodUri)); - Assert.IsTrue(new UriIdentifier(this.goodUri).Equals(this.goodUri)); + TestAsFullAndPartialTrust(fulltrust => { + Assert.AreEqual(new UriIdentifier(this.goodUri), new UriIdentifier(this.goodUri)); + // This next test is an interesting side-effect of passing off to Uri.Equals. But it's probably ok. + Assert.AreEqual(new UriIdentifier(this.goodUri), new UriIdentifier(this.goodUri + "#frag")); + Assert.AreEqual(new UriIdentifier("http://a/b./c."), new UriIdentifier("http://a/b./c.#frag")); + Assert.AreNotEqual(new UriIdentifier(this.goodUri), new UriIdentifier(this.goodUri + "a")); + Assert.AreNotEqual(null, new UriIdentifier(this.goodUri)); + Assert.IsTrue(new UriIdentifier(this.goodUri).Equals(this.goodUri)); + + Assert.AreEqual(Identifier.Parse("HTTP://WWW.FOO.COM/abc", true), Identifier.Parse("http://www.foo.com/abc", true)); + Assert.AreEqual(Identifier.Parse("HTTP://WWW.FOO.COM/abc", true), Identifier.Parse("http://www.foo.com/abc", false)); + Assert.AreEqual(Identifier.Parse("HTTP://WWW.FOO.COM/abc", false), Identifier.Parse("http://www.foo.com/abc", false)); + Assert.AreNotEqual(Identifier.Parse("http://www.foo.com/abc", true), Identifier.Parse("http://www.foo.com/ABC", true)); + Assert.AreNotEqual(Identifier.Parse("http://www.foo.com/abc", true), Identifier.Parse("http://www.foo.com/ABC", false)); + Assert.AreNotEqual(Identifier.Parse("http://www.foo.com/abc", false), Identifier.Parse("http://www.foo.com/ABC", false)); + + Assert.AreNotEqual(Identifier.Parse("http://a/b./c."), Identifier.Parse("http://a/b/c.")); + Assert.AreEqual(Identifier.Parse("http://a/b./c."), Identifier.Parse("http://a/b./c.")); + }); } [TestCase] @@ -150,6 +179,51 @@ namespace DotNetOpenAuth.Test.OpenId { Assert.AreEqual("https://host:80/PaTH?KeY=VaLUE#fRag", id.ToString()); } + /// <summary> + /// Verifies that URIs that contain base64 encoded path segments (such as Yahoo) are properly preserved. + /// </summary> + /// <remarks> + /// Yahoo includes a base64 encoded part as their last path segment, + /// which may end with a period. The default .NET Uri parser trims off + /// trailing periods, which breaks OpenID unless special precautions are taken. + /// </remarks> + [TestCase] + public void TrailingPeriodsNotTrimmed() { + TestAsFullAndPartialTrust(fullTrust => { + string claimedIdentifier = "https://me.yahoo.com/a/AsDf.#asdf"; + Identifier id = claimedIdentifier; + Assert.AreEqual(claimedIdentifier, id.OriginalString); + Assert.AreEqual(claimedIdentifier, id.ToString()); + + UriIdentifier idUri = new UriIdentifier(claimedIdentifier); + Assert.AreEqual(claimedIdentifier, idUri.OriginalString); + Assert.AreEqual(claimedIdentifier, idUri.ToString()); + if (fullTrust) { + Assert.AreEqual(claimedIdentifier, idUri.Uri.AbsoluteUri); + } + Assert.AreEqual(Uri.UriSchemeHttps, idUri.Uri.Scheme); // in case custom scheme tricks are played, this must still match + Assert.AreEqual("https://me.yahoo.com/a/AsDf.", idUri.TrimFragment().ToString()); + Assert.AreEqual("https://me.yahoo.com/a/AsDf.", idUri.TrimFragment().OriginalString); + Assert.AreEqual(id.ToString(), new UriIdentifier((Uri)idUri).ToString(), "Round tripping UriIdentifier->Uri->UriIdentifier failed."); + + idUri = new UriIdentifier(new Uri(claimedIdentifier)); + Assert.AreEqual(claimedIdentifier, idUri.OriginalString); + Assert.AreEqual(claimedIdentifier, idUri.ToString()); + if (fullTrust) { + Assert.AreEqual(claimedIdentifier, idUri.Uri.AbsoluteUri); + } + Assert.AreEqual(Uri.UriSchemeHttps, idUri.Uri.Scheme); // in case custom scheme tricks are played, this must still match + Assert.AreEqual("https://me.yahoo.com/a/AsDf.", idUri.TrimFragment().ToString()); + Assert.AreEqual("https://me.yahoo.com/a/AsDf.", idUri.TrimFragment().OriginalString); + Assert.AreEqual(id.ToString(), new UriIdentifier((Uri)idUri).ToString(), "Round tripping UriIdentifier->Uri->UriIdentifier failed."); + + claimedIdentifier = "https://me.yahoo.com:443/a/AsDf.#asdf"; + id = claimedIdentifier; + Assert.AreEqual(claimedIdentifier, id.OriginalString); + Assert.AreEqual("https://me.yahoo.com/a/AsDf.#asdf", id.ToString()); + }); + } + [TestCase] public void HttpSchemePrepended() { UriIdentifier id = new UriIdentifier("www.yahoo.com"); @@ -193,5 +267,45 @@ namespace DotNetOpenAuth.Test.OpenId { Assert.AreEqual("http://www.yahoo.com/", secureId.ToString()); Assert.AreEqual(0, Discover(secureId).Count()); } + + /// <summary> + /// Verifies that unicode hostnames are handled. + /// </summary> + [TestCase] + public void UnicodeHostSupport() { + var id = new UriIdentifier("http://server崎/村"); + Assert.AreEqual("server崎", id.Uri.Host); + } + + /// <summary> + /// Verifies SimpleUri behavior + /// </summary> + [TestCase] + public void SimpleUri() { + Assert.AreEqual("http://abc/D./e.?Qq#Ff", new UriIdentifier.SimpleUri("HTTP://ABC/D./e.?Qq#Ff").ToString()); + Assert.AreEqual("http://abc/D./e.?Qq", new UriIdentifier.SimpleUri("HTTP://ABC/D./e.?Qq").ToString()); + Assert.AreEqual("http://abc/D./e.#Ff", new UriIdentifier.SimpleUri("HTTP://ABC/D./e.#Ff").ToString()); + Assert.AreEqual("http://abc/", new UriIdentifier.SimpleUri("HTTP://ABC/").ToString()); + Assert.AreEqual("http://abc/", new UriIdentifier.SimpleUri("HTTP://ABC").ToString()); + Assert.AreEqual("http://abc/?q", new UriIdentifier.SimpleUri("HTTP://ABC?q").ToString()); + Assert.AreEqual("http://abc/#f", new UriIdentifier.SimpleUri("HTTP://ABC#f").ToString()); + + Assert.AreEqual("http://abc/a//b", new UriIdentifier.SimpleUri("http://abc/a//b").ToString()); + Assert.AreEqual("http://abc/a%2Fb/c", new UriIdentifier.SimpleUri("http://abc/a%2fb/c").ToString()); + Assert.AreEqual("http://abc/A/c", new UriIdentifier.SimpleUri("http://abc/%41/c").ToString()); + } + + private static void TestAsFullAndPartialTrust(Action<bool> action) { + // Test a bunch of interesting URLs both with scheme substitution on and off. + Assert.IsTrue(UriIdentifier_Accessor.schemeSubstitution, "Expected scheme substitution to be working."); + action(true); + + UriIdentifier_Accessor.schemeSubstitution = false; + try { + action(false); + } finally { + UriIdentifier_Accessor.schemeSubstitution = true; + } + } } } diff --git a/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs b/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs index 37f9c78..980d90f 100644 --- a/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs +++ b/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs @@ -144,6 +144,7 @@ using System.Reflection; /// The conversion cannot be performed. /// </exception> public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { + Contract.Assume(destinationType != null, "Missing contract."); if (destinationType.IsInstanceOfType(value)) { return value; } diff --git a/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs b/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs index 6ba9c4b..61c0fd8 100644 --- a/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs +++ b/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs @@ -45,7 +45,7 @@ namespace DotNetOpenAuth.ComponentModel { return null; } - MemberInfo identifierParse = typeof(Identifier).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public); + MemberInfo identifierParse = typeof(Identifier).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof(string) }, null); return CreateInstanceDescriptor(identifierParse, new object[] { value.ToString() }); } diff --git a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd index a4f932e..3164ec5 100644 --- a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd +++ b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd @@ -319,7 +319,24 @@ </xs:documentation> </xs:annotation> </xs:attribute> - <xs:attribute name="allowDualPurposeIdentifiers" type="xs:boolean" /> + <xs:attribute name="allowDualPurposeIdentifiers" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Controls whether identifiers that are both OP Identifiers and Claimed Identifiers + should ever be recognized as claimed identifiers. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="allowApproximateIdentifierDiscovery" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Controls whether certain Claimed Identifiers that exploit + features that .NET does not have the ability to send exact HTTP requests for will + still be allowed by using an approximate HTTP request. + Only impacts hosts running under partial trust. + </xs:documentation> + </xs:annotation> + </xs:attribute> <xs:attribute name="protectDownlevelReplayAttacks" type="xs:boolean"> <xs:annotation> <xs:documentation> diff --git a/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs b/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs index 1e3df8f..1bf2ebc 100644 --- a/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs +++ b/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs @@ -71,6 +71,11 @@ namespace DotNetOpenAuth.Configuration { private const string AllowDualPurposeIdentifiersConfigName = "allowDualPurposeIdentifiers"; /// <summary> + /// Gets the name of the @allowApproximateIdentifierDiscovery attribute. + /// </summary> + private const string AllowApproximateIdentifierDiscoveryConfigName = "allowApproximateIdentifierDiscovery"; + + /// <summary> /// Gets the name of the @protectDownlevelReplayAttacks attribute. /// </summary> private const string ProtectDownlevelReplayAttacksConfigName = "protectDownlevelReplayAttacks"; @@ -206,6 +211,20 @@ namespace DotNetOpenAuth.Configuration { } /// <summary> + /// Gets or sets a value indicating whether certain Claimed Identifiers that exploit + /// features that .NET does not have the ability to send exact HTTP requests for will + /// still be allowed by using an approximate HTTP request. + /// </summary> + /// <value> + /// The default value is <c>true</c>. + /// </value> + [ConfigurationProperty(AllowApproximateIdentifierDiscoveryConfigName, DefaultValue = true)] + public bool AllowApproximateIdentifierDiscovery { + get { return (bool)this[AllowApproximateIdentifierDiscoveryConfigName]; } + set { this[AllowApproximateIdentifierDiscoveryConfigName] = value; } + } + + /// <summary> /// Gets or sets a value indicating whether the Relying Party should take special care /// to protect users against replay attacks when interoperating with OpenID 1.1 Providers. /// </summary> @@ -234,6 +253,7 @@ namespace DotNetOpenAuth.Configuration { settings.RejectDelegatingIdentifiers = this.RejectDelegatingIdentifiers; settings.IgnoreUnsignedExtensions = this.IgnoreUnsignedExtensions; settings.AllowDualPurposeIdentifiers = this.AllowDualPurposeIdentifiers; + settings.AllowApproximateIdentifierDiscovery = this.AllowApproximateIdentifierDiscovery; settings.ProtectDownlevelReplayAttacks = this.ProtectDownlevelReplayAttacks; return settings; diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 3dee328..1c018eb 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -163,7 +163,7 @@ http://opensource.org/licenses/ms-pl.html <CodeContractsEmitXMLDocs>True</CodeContractsEmitXMLDocs> <CodeContractsRedundantAssumptions>False</CodeContractsRedundantAssumptions> <CodeContractsReferenceAssembly>Build</CodeContractsReferenceAssembly> - <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet> + <CodeAnalysisRuleSet>Migrated rules for DotNetOpenAuth.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> <Reference Include="log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=1b44e1d426115821, processorArchitecture=MSIL"> @@ -736,7 +736,7 @@ http://opensource.org/licenses/ms-pl.html <ItemGroup> <SignDependsOn Include="BuildUnifiedProduct" /> <DelaySignedAssemblies Include="$(ILMergeOutputAssembly); - $(OutputPath)$(ProductName).Contracts.dll; + $(OutputPath)CodeContracts\$(ProductName).Contracts.dll; " /> </ItemGroup> <PropertyGroup> diff --git a/src/DotNetOpenAuth/GlobalSuppressions.cs b/src/DotNetOpenAuth/GlobalSuppressions.cs index e436846..9b1bcfa 100644 --- a/src/DotNetOpenAuth/GlobalSuppressions.cs +++ b/src/DotNetOpenAuth/GlobalSuppressions.cs @@ -57,3 +57,6 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.Provider.IProviderBehavior.OnIncomingRequest(DotNetOpenAuth.OpenId.Provider.IRequest)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.Provider.IProviderBehavior.ApplySecuritySettings(DotNetOpenAuth.OpenId.Provider.ProviderSecuritySettings)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2243:AttributeStringLiteralsShouldParseCorrectly")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.Mvc")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Mvc", Scope = "namespace", Target = "DotNetOpenAuth.Mvc")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Portability", "CA1903:UseOnlyApiFromTargetedFramework", MessageId = "System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")] diff --git a/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs b/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs index 86c1118..ae45229 100644 --- a/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs +++ b/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs @@ -268,6 +268,7 @@ namespace DotNetOpenAuth.InfoCard { [Category(InfoCardCategory), DefaultValue(PrivacyUrlDefault)] [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "We construct a Uri to validate the format of the string.")] [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "That overload is NOT the same.")] + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "This can take ~/ paths.")] public string PrivacyUrl { get { return (string)this.ViewState[PrivacyUrlViewStateKey] ?? PrivacyUrlDefault; @@ -570,24 +571,28 @@ namespace DotNetOpenAuth.InfoCard { Panel supportedPanel = new Panel(); - if (!this.DesignMode) { - // At the user agent, assume InfoCard is not supported until - // the JavaScript discovers otherwise and reveals this panel. - supportedPanel.Style[HtmlTextWriterStyle.Display] = "none"; - } + try { + if (!this.DesignMode) { + // At the user agent, assume InfoCard is not supported until + // the JavaScript discovers otherwise and reveals this panel. + supportedPanel.Style[HtmlTextWriterStyle.Display] = "none"; + } - supportedPanel.Controls.Add(this.CreateInfoCardImage()); + supportedPanel.Controls.Add(this.CreateInfoCardImage()); - // trigger the selector at page load? - if (this.AutoPopup && !this.Page.IsPostBack) { - this.Page.ClientScript.RegisterStartupScript( - typeof(InfoCardSelector), - "selector_load_trigger", - this.GetInfoCardSelectorActivationScript(true), - true); + // trigger the selector at page load? + if (this.AutoPopup && !this.Page.IsPostBack) { + this.Page.ClientScript.RegisterStartupScript( + typeof(InfoCardSelector), + "selector_load_trigger", + this.GetInfoCardSelectorActivationScript(true), + true); + } + return supportedPanel; + } catch { + supportedPanel.Dispose(); + throw; } - - return supportedPanel; } /// <summary> @@ -624,10 +629,15 @@ namespace DotNetOpenAuth.InfoCard { Contract.Ensures(Contract.Result<Panel>() != null); Panel unsupportedPanel = new Panel(); - if (this.UnsupportedTemplate != null) { - this.UnsupportedTemplate.InstantiateIn(unsupportedPanel); + try { + if (this.UnsupportedTemplate != null) { + this.UnsupportedTemplate.InstantiateIn(unsupportedPanel); + } + return unsupportedPanel; + } catch { + unsupportedPanel.Dispose(); + throw; } - return unsupportedPanel; } /// <summary> @@ -692,13 +702,18 @@ namespace DotNetOpenAuth.InfoCard { private Image CreateInfoCardImage() { // add clickable image Image image = new Image(); - image.ImageUrl = this.Page.ClientScript.GetWebResourceUrl(typeof(InfoCardSelector), InfoCardImage.GetImageManifestResourceStreamName(this.ImageSize)); - image.AlternateText = InfoCardStrings.SelectorClickPrompt; - image.ToolTip = this.ToolTip; - image.Style[HtmlTextWriterStyle.Cursor] = "hand"; - - image.Attributes["onclick"] = this.GetInfoCardSelectorActivationScript(false); - return image; + try { + image.ImageUrl = this.Page.ClientScript.GetWebResourceUrl(typeof(InfoCardSelector), InfoCardImage.GetImageManifestResourceStreamName(this.ImageSize)); + image.AlternateText = InfoCardStrings.SelectorClickPrompt; + image.ToolTip = this.ToolTip; + image.Style[HtmlTextWriterStyle.Cursor] = "hand"; + + image.Attributes["onclick"] = this.GetInfoCardSelectorActivationScript(false); + return image; + } catch { + image.Dispose(); + throw; + } } /// <summary> diff --git a/src/DotNetOpenAuth/InfoCard/ReceivingTokenEventArgs.cs b/src/DotNetOpenAuth/InfoCard/ReceivingTokenEventArgs.cs index 124f9f8..2ac2b7e 100644 --- a/src/DotNetOpenAuth/InfoCard/ReceivingTokenEventArgs.cs +++ b/src/DotNetOpenAuth/InfoCard/ReceivingTokenEventArgs.cs @@ -74,7 +74,13 @@ namespace DotNetOpenAuth.InfoCard { public void AddDecryptingToken(X509Certificate2 certificate) { Contract.Requires<ArgumentNullException>(certificate != null); Contract.Requires<ArgumentException>(certificate.HasPrivateKey); - this.AddDecryptingToken(new X509SecurityToken(certificate)); + var cert = new X509SecurityToken(certificate); + try { + this.AddDecryptingToken(cert); + } catch { + cert.Dispose(); + throw; + } } #if CONTRACTS_FULL diff --git a/src/DotNetOpenAuth/InfoCard/Token/Token.cs b/src/DotNetOpenAuth/InfoCard/Token/Token.cs index 7fa9a95..89fa3a3 100644 --- a/src/DotNetOpenAuth/InfoCard/Token/Token.cs +++ b/src/DotNetOpenAuth/InfoCard/Token/Token.cs @@ -49,16 +49,18 @@ namespace DotNetOpenAuth.InfoCard { byte[] decryptedBytes; string decryptedString; - using (XmlReader tokenReader = XmlReader.Create(new StringReader(tokenXml))) { - Contract.Assume(tokenReader != null); // BCL contract should say XmlReader.Create result != null - if (IsEncrypted(tokenReader)) { - Logger.InfoCard.DebugFormat("Incoming SAML token, before decryption: {0}", tokenXml); - decryptedBytes = decryptor.DecryptToken(tokenReader); - decryptedString = Encoding.UTF8.GetString(decryptedBytes); - Contract.Assume(decryptedString != null); // BCL contracts should be enhanced here - } else { - decryptedBytes = Encoding.UTF8.GetBytes(tokenXml); - decryptedString = tokenXml; + using (StringReader xmlReader = new StringReader(tokenXml)) { + using (XmlReader tokenReader = XmlReader.Create(xmlReader)) { + Contract.Assume(tokenReader != null); // BCL contract should say XmlReader.Create result != null + if (IsEncrypted(tokenReader)) { + Logger.InfoCard.DebugFormat("Incoming SAML token, before decryption: {0}", tokenXml); + decryptedBytes = decryptor.DecryptToken(tokenReader); + decryptedString = Encoding.UTF8.GetString(decryptedBytes); + Contract.Assume(decryptedString != null); // BCL contracts should be enhanced here + } else { + decryptedBytes = Encoding.UTF8.GetBytes(tokenXml); + decryptedString = tokenXml; + } } } diff --git a/src/DotNetOpenAuth/InfoCard/Token/TokenUtility.cs b/src/DotNetOpenAuth/InfoCard/Token/TokenUtility.cs index 48b7794..4ac871a 100644 --- a/src/DotNetOpenAuth/InfoCard/Token/TokenUtility.cs +++ b/src/DotNetOpenAuth/InfoCard/Token/TokenUtility.cs @@ -226,7 +226,9 @@ namespace DotNetOpenAuth.InfoCard { int charMapLength = charMap.Length; byte[] raw = Convert.FromBase64String(ppid); - raw = SHA1.Create().ComputeHash(raw); + using (HashAlgorithm hasher = SHA1.Create()) { + raw = hasher.ComputeHash(raw); + } StringBuilder callSign = new StringBuilder(); diff --git a/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs b/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs index dd34d90..c9bc1d3 100644 --- a/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs +++ b/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs @@ -90,11 +90,16 @@ namespace DotNetOpenAuth.Messaging { public override StreamReader GetResponseReader() { this.ResponseStream.Seek(0, SeekOrigin.Begin); string contentEncoding = this.Headers[HttpResponseHeader.ContentEncoding]; - if (string.IsNullOrEmpty(contentEncoding)) { - return new StreamReader(this.ResponseStream); - } else { - return new StreamReader(this.ResponseStream, Encoding.GetEncoding(contentEncoding)); + Encoding encoding = null; + if (!string.IsNullOrEmpty(contentEncoding)) { + try { + encoding = Encoding.GetEncoding(contentEncoding); + } catch (ArgumentException ex) { + Logger.Messaging.ErrorFormat("Encoding.GetEncoding(\"{0}\") threw ArgumentException: {1}", contentEncoding, ex); + } } + + return encoding != null ? new StreamReader(this.ResponseStream, encoding) : new StreamReader(this.ResponseStream); } /// <summary> diff --git a/src/DotNetOpenAuth/Messaging/Channel.cs b/src/DotNetOpenAuth/Messaging/Channel.cs index 7198c78..055ce68 100644 --- a/src/DotNetOpenAuth/Messaging/Channel.cs +++ b/src/DotNetOpenAuth/Messaging/Channel.cs @@ -695,27 +695,28 @@ namespace DotNetOpenAuth.Messaging { WebHeaderCollection headers = new WebHeaderCollection(); headers.Add(HttpResponseHeader.ContentType, "text/html"); - StringWriter bodyWriter = new StringWriter(CultureInfo.InvariantCulture); - StringBuilder hiddenFields = new StringBuilder(); - foreach (var field in fields) { - hiddenFields.AppendFormat( - "\t<input type=\"hidden\" name=\"{0}\" value=\"{1}\" />\r\n", - HttpUtility.HtmlEncode(field.Key), - HttpUtility.HtmlEncode(field.Value)); + using (StringWriter bodyWriter = new StringWriter(CultureInfo.InvariantCulture)) { + StringBuilder hiddenFields = new StringBuilder(); + foreach (var field in fields) { + hiddenFields.AppendFormat( + "\t<input type=\"hidden\" name=\"{0}\" value=\"{1}\" />\r\n", + HttpUtility.HtmlEncode(field.Key), + HttpUtility.HtmlEncode(field.Value)); + } + bodyWriter.WriteLine( + IndirectMessageFormPostFormat, + HttpUtility.HtmlEncode(message.Recipient.AbsoluteUri), + hiddenFields); + bodyWriter.Flush(); + OutgoingWebResponse response = new OutgoingWebResponse { + Status = HttpStatusCode.OK, + Headers = headers, + Body = bodyWriter.ToString(), + OriginalMessage = message + }; + + return response; } - bodyWriter.WriteLine( - IndirectMessageFormPostFormat, - HttpUtility.HtmlEncode(message.Recipient.AbsoluteUri), - hiddenFields); - bodyWriter.Flush(); - OutgoingWebResponse response = new OutgoingWebResponse { - Status = HttpStatusCode.OK, - Headers = headers, - Body = bodyWriter.ToString(), - OriginalMessage = message - }; - - return response; } /// <summary> diff --git a/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs b/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs index 0bbac42..6f8c4f9 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30104.0 +// Runtime Version:4.0.30319.1 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -394,6 +394,15 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Looks up a localized string similar to An HttpContext.Current.Session object is required.. + /// </summary> + internal static string SessionRequired { + get { + return ResourceManager.GetString("SessionRequired", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Message signature was incorrect.. /// </summary> internal static string SignatureInvalid { diff --git a/src/DotNetOpenAuth/Messaging/MessagingStrings.resx b/src/DotNetOpenAuth/Messaging/MessagingStrings.resx index 34385d4..bdf4f68 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingStrings.resx +++ b/src/DotNetOpenAuth/Messaging/MessagingStrings.resx @@ -300,4 +300,7 @@ <data name="BinaryDataRequiresMultipart" xml:space="preserve"> <value>Unable to send all message data because some of it requires multi-part POST, but IMessageWithBinaryData.SendAsMultipart was false.</value> </data> + <data name="SessionRequired" xml:space="preserve"> + <value>An HttpContext.Current.Session object is required.</value> + </data> </root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index 7367c01..231637a 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -89,7 +89,7 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Transforms an OutgoingWebResponse to an MVC-friendly ActionResult. /// </summary> - /// <param name="response">The response to send to the uesr agent.</param> + /// <param name="response">The response to send to the user agent.</param> /// <returns>The <see cref="ActionResult"/> instance to be returned by the Controller's action method.</returns> public static ActionResult AsActionResult(this OutgoingWebResponse response) { Contract.Requires<ArgumentNullException>(response != null); @@ -181,7 +181,10 @@ namespace DotNetOpenAuth.Messaging { /// <typeparam name="T">The type of element contained in the sequence.</typeparam> /// <param name="sequence">The sequence of sequences to flatten.</param> /// <returns>A sequence of the contained items.</returns> + [Obsolete("Use Enumerable.SelectMany instead.")] public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> sequence) { + ErrorUtilities.VerifyArgumentNotNull(sequence, "sequence"); + foreach (IEnumerable<T> subsequence in sequence) { foreach (T item in subsequence) { yield return item; diff --git a/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs b/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs index e9613ab..3524f41 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs @@ -73,10 +73,10 @@ namespace DotNetOpenAuth.Messaging.Reflection { Contract.Assume(str != null); return bool.Parse(str); }; - Func<string, Identifier> safeIdentfier = str => { + Func<string, Identifier> safeIdentifier = str => { Contract.Assume(str != null); ErrorUtilities.VerifyFormat(str.Length > 0, MessagingStrings.NonEmptyStringExpected); - return Identifier.Parse(str); + return Identifier.Parse(str, true); }; Func<byte[], string> safeFromByteArray = bytes => { Contract.Assume(bytes != null); @@ -94,7 +94,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { Map<DateTime>(dt => XmlConvert.ToString(dt, XmlDateTimeSerializationMode.Utc), str => XmlConvert.ToDateTime(str, XmlDateTimeSerializationMode.Utc)); Map<byte[]>(safeFromByteArray, safeToByteArray); Map<Realm>(realm => realm.ToString(), safeRealm); - Map<Identifier>(id => id.ToString(), safeIdentfier); + Map<Identifier>(id => id.SerializedString, safeIdentifier); Map<bool>(value => value.ToString().ToLowerInvariant(), safeBool); Map<CultureInfo>(c => c.Name, str => new CultureInfo(str)); Map<CultureInfo[]>(cs => string.Join(",", cs.Select(c => c.Name).ToArray()), str => str.Split(',').Select(s => new CultureInfo(s)).ToArray()); diff --git a/src/DotNetOpenAuth/Migrated rules for DotNetOpenAuth.ruleset b/src/DotNetOpenAuth/Migrated rules for DotNetOpenAuth.ruleset index cee6f53..db238b6 100644 --- a/src/DotNetOpenAuth/Migrated rules for DotNetOpenAuth.ruleset +++ b/src/DotNetOpenAuth/Migrated rules for DotNetOpenAuth.ruleset @@ -5,5 +5,6 @@ <Rule Id="CA1054" Action="None" /> <Rule Id="CA1055" Action="None" /> <Rule Id="CA1056" Action="None" /> + <Rule Id="CA2104" Action="None" /> </Rules> </RuleSet>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs index b45da66..cf09036 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs @@ -258,6 +258,8 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// <c>true</c> if the signature on the message is valid; otherwise, <c>false</c>. /// </returns> protected virtual bool IsSignatureValid(ITamperResistantOAuthMessage message) { + Contract.Requires<ArgumentNullException>(message != null); + string signature = this.GetSignature(message); return message.Signature == signature; } diff --git a/src/DotNetOpenAuth/OpenId/Association.cs b/src/DotNetOpenAuth/OpenId/Association.cs index 62e91ec..3c7e89f 100644 --- a/src/DotNetOpenAuth/OpenId/Association.cs +++ b/src/DotNetOpenAuth/OpenId/Association.cs @@ -238,24 +238,28 @@ namespace DotNetOpenAuth.OpenId { /// </returns> public override int GetHashCode() { HMACSHA1 hmac = new HMACSHA1(this.SecretKey); - CryptoStream cs = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write); + try { + CryptoStream cs = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write); - byte[] hbytes = ASCIIEncoding.ASCII.GetBytes(this.Handle); + byte[] hbytes = ASCIIEncoding.ASCII.GetBytes(this.Handle); - cs.Write(hbytes, 0, hbytes.Length); - cs.Close(); + cs.Write(hbytes, 0, hbytes.Length); + cs.Close(); - byte[] hash = hmac.Hash; - hmac.Clear(); + byte[] hash = hmac.Hash; + hmac.Clear(); - long val = 0; - for (int i = 0; i < hash.Length; i++) { - val = val ^ (long)hash[i]; - } + long val = 0; + for (int i = 0; i < hash.Length; i++) { + val = val ^ (long)hash[i]; + } - val = val ^ this.Expires.ToFileTimeUtc(); + val = val ^ this.Expires.ToFileTimeUtc(); - return (int)val; + return (int)val; + } finally { + ((IDisposable)hmac).Dispose(); + } } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs b/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs index 76a1c9b..f178647 100644 --- a/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs @@ -61,6 +61,7 @@ namespace DotNetOpenAuth.OpenId.Extensions.UI { /// </summary> public UIRequest() { this.LanguagePreference = new[] { CultureInfo.CurrentUICulture }; + this.Mode = UIModes.Popup; } /// <summary> @@ -75,12 +76,11 @@ namespace DotNetOpenAuth.OpenId.Extensions.UI { public CultureInfo[] LanguagePreference { get; set; } /// <summary> - /// Gets the style of UI that the RP is hosting the OP's authentication page in. + /// Gets or sets the style of UI that the RP is hosting the OP's authentication page in. /// </summary> /// <value>Some value from the <see cref="UIModes"/> class. Defaults to <see cref="UIModes.Popup"/>.</value> - [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Design is to allow this later to be changable when more than one value exists.")] [MessagePart("mode", AllowEmpty = false, IsRequired = true)] - public string Mode { get { return UIModes.Popup; } } + public string Mode { get; set; } /// <summary> /// Gets or sets a value indicating whether the Relying Party has an icon diff --git a/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs b/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs index e96f362..ba9852e 100644 --- a/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs +++ b/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs @@ -110,29 +110,29 @@ namespace DotNetOpenAuth.OpenId { var results = new List<IdentifierDiscoveryResult>(); string signingHost; - var response = GetXrdsResponse(uriIdentifier, requestHandler, out signingHost); - - if (response != null) { - try { - var document = new XrdsDocument(XmlReader.Create(response.ResponseStream)); - ValidateXmlDSig(document, uriIdentifier, response, signingHost); - var xrds = GetXrdElements(document, uriIdentifier.Uri.Host); - - // Look for claimed identifier template URIs for an additional XRDS document. - results.AddRange(GetExternalServices(xrds, uriIdentifier, requestHandler)); - - // If we couldn't find any claimed identifiers, look for OP identifiers. - // Normally this would be the opposite (OP Identifiers take precedence over - // claimed identifiers, but for Google Apps, XRDS' always have OP Identifiers - // mixed in, which the OpenID spec mandate should eclipse Claimed Identifiers, - // which would break positive assertion checks). - if (results.Count == 0) { - results.AddRange(xrds.CreateServiceEndpoints(uriIdentifier, uriIdentifier)); + using (var response = GetXrdsResponse(uriIdentifier, requestHandler, out signingHost)) { + if (response != null) { + try { + var document = new XrdsDocument(XmlReader.Create(response.ResponseStream)); + ValidateXmlDSig(document, uriIdentifier, response, signingHost); + var xrds = GetXrdElements(document, uriIdentifier.Uri.Host); + + // Look for claimed identifier template URIs for an additional XRDS document. + results.AddRange(GetExternalServices(xrds, uriIdentifier, requestHandler)); + + // If we couldn't find any claimed identifiers, look for OP identifiers. + // Normally this would be the opposite (OP Identifiers take precedence over + // claimed identifiers, but for Google Apps, XRDS' always have OP Identifiers + // mixed in, which the OpenID spec mandate should eclipse Claimed Identifiers, + // which would break positive assertion checks). + if (results.Count == 0) { + results.AddRange(xrds.CreateServiceEndpoints(uriIdentifier, uriIdentifier)); + } + + abortDiscoveryChain = true; + } catch (XmlException ex) { + Logger.Yadis.ErrorFormat("Error while parsing XRDS document at {0} pointed to by host-meta: {1}", response.FinalUri, ex); } - - abortDiscoveryChain = true; - } catch (XmlException ex) { - Logger.Yadis.ErrorFormat("Error while parsing XRDS document at {0} pointed to by host-meta: {1}", response.FinalUri, ex); } } @@ -188,10 +188,11 @@ namespace DotNetOpenAuth.OpenId { Uri externalLocation = new Uri(templateNode.Value.Trim().Replace("{%uri}", Uri.EscapeDataString(identifier.Uri.AbsoluteUri))); string nextAuthority = nextAuthorityNode != null ? nextAuthorityNode.Value.Trim() : identifier.Uri.Host; try { - var externalXrdsResponse = GetXrdsResponse(identifier, requestHandler, externalLocation); - XrdsDocument externalXrds = new XrdsDocument(XmlReader.Create(externalXrdsResponse.ResponseStream)); - ValidateXmlDSig(externalXrds, identifier, externalXrdsResponse, nextAuthority); - results.AddRange(GetXrdElements(externalXrds, identifier).CreateServiceEndpoints(identifier, identifier)); + using (var externalXrdsResponse = GetXrdsResponse(identifier, requestHandler, externalLocation)) { + XrdsDocument externalXrds = new XrdsDocument(XmlReader.Create(externalXrdsResponse.ResponseStream)); + ValidateXmlDSig(externalXrds, identifier, externalXrdsResponse, nextAuthority); + results.AddRange(GetXrdElements(externalXrds, identifier).CreateServiceEndpoints(identifier, identifier)); + } } catch (ProtocolException ex) { Logger.Yadis.WarnFormat("HTTP GET error while retrieving described-by XRDS document {0}: {1}", externalLocation.AbsoluteUri, ex); } catch (XmlException ex) { @@ -217,9 +218,9 @@ namespace DotNetOpenAuth.OpenId { Contract.Requires<ArgumentNullException>(response != null); var signatureNode = document.Node.SelectSingleNode("/xrds:XRDS/ds:Signature", document.XmlNamespaceResolver); - ErrorUtilities.VerifyProtocol(signatureNode != null, "Missing Signature element."); + ErrorUtilities.VerifyProtocol(signatureNode != null, OpenIdStrings.MissingElement, "Signature"); var signedInfoNode = signatureNode.SelectSingleNode("ds:SignedInfo", document.XmlNamespaceResolver); - ErrorUtilities.VerifyProtocol(signedInfoNode != null, "Missing SignedInfo element."); + ErrorUtilities.VerifyProtocol(signedInfoNode != null, OpenIdStrings.MissingElement, "SignedInfo"); ErrorUtilities.VerifyProtocol( signedInfoNode.SelectSingleNode("ds:CanonicalizationMethod[@Algorithm='http://docs.oasis-open.org/xri/xrd/2009/01#canonicalize-raw-octets']", document.XmlNamespaceResolver) != null, "Unrecognized or missing canonicalization method."); @@ -227,16 +228,19 @@ namespace DotNetOpenAuth.OpenId { signedInfoNode.SelectSingleNode("ds:SignatureMethod[@Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1']", document.XmlNamespaceResolver) != null, "Unrecognized or missing signature method."); var certNodes = signatureNode.Select("ds:KeyInfo/ds:X509Data/ds:X509Certificate", document.XmlNamespaceResolver); - ErrorUtilities.VerifyProtocol(certNodes.Count > 0, "Missing X509Certificate element."); + ErrorUtilities.VerifyProtocol(certNodes.Count > 0, OpenIdStrings.MissingElement, "X509Certificate"); var certs = certNodes.Cast<XPathNavigator>().Select(n => new X509Certificate2(Convert.FromBase64String(n.Value.Trim()))).ToList(); // Verify that we trust the signer of the certificates. // Start by trying to validate just the certificate used to sign the XRDS document, // since we can do that with partial trust. + Logger.OpenId.Debug("Verifying that we trust the certificate used to sign the discovery document."); if (!certs[0].Verify()) { // We couldn't verify just the signing certificate, so try to verify the whole certificate chain. try { + Logger.OpenId.Debug("Verifying the whole certificate chain."); VerifyCertChain(certs); + Logger.OpenId.Debug("Certificate chain verified."); } catch (SecurityException) { Logger.Yadis.Warn("Signing certificate verification failed and we have insufficient code access security permissions to perform certificate chain validation."); ErrorUtilities.ThrowProtocol(OpenIdStrings.X509CertificateNotTrusted); @@ -303,7 +307,7 @@ namespace DotNetOpenAuth.OpenId { request.CachePolicy = Yadis.IdentifierDiscoveryCachePolicy; request.Accept = ContentTypes.Xrds; var options = identifier.IsDiscoverySecureEndToEnd ? DirectWebRequestOptions.RequireSsl : DirectWebRequestOptions.None; - var response = requestHandler.GetResponse(request, options); + var response = requestHandler.GetResponse(request, options).GetSnapshot(Yadis.MaximumResultToScan); if (!string.Equals(response.ContentType.MediaType, ContentTypes.Xrds, StringComparison.Ordinal)) { Logger.Yadis.WarnFormat("Host-meta pointed to XRDS at {0}, but Content-Type at that URL was unexpected value '{1}'.", xrdsLocation, response.ContentType); } @@ -342,23 +346,24 @@ namespace DotNetOpenAuth.OpenId { private Uri GetXrdsLocation(UriIdentifier identifier, IDirectWebRequestHandler requestHandler, out string signingHost) { Contract.Requires<ArgumentNullException>(identifier != null); Contract.Requires<ArgumentNullException>(requestHandler != null); - var hostMetaResponse = this.GetHostMeta(identifier, requestHandler, out signingHost); - if (hostMetaResponse == null) { - return null; - } + using (var hostMetaResponse = this.GetHostMeta(identifier, requestHandler, out signingHost)) { + if (hostMetaResponse == null) { + return null; + } - using (var sr = hostMetaResponse.GetResponseReader()) { - string line = sr.ReadLine(); - Match m = HostMetaLink.Match(line); - if (m.Success) { - Uri location = new Uri(m.Groups["location"].Value); - Logger.Yadis.InfoFormat("Found link to XRDS at {0} in host-meta document {1}.", location, hostMetaResponse.FinalUri); - return location; + using (var sr = hostMetaResponse.GetResponseReader()) { + string line = sr.ReadLine(); + Match m = HostMetaLink.Match(line); + if (m.Success) { + Uri location = new Uri(m.Groups["location"].Value); + Logger.Yadis.InfoFormat("Found link to XRDS at {0} in host-meta document {1}.", location, hostMetaResponse.FinalUri); + return location; + } } - } - Logger.Yadis.WarnFormat("Could not find link to XRDS in host-meta document: {0}", hostMetaResponse.FinalUri); - return null; + Logger.Yadis.WarnFormat("Could not find link to XRDS in host-meta document: {0}", hostMetaResponse.FinalUri); + return null; + } } /// <summary> @@ -381,13 +386,19 @@ namespace DotNetOpenAuth.OpenId { if (identifier.IsDiscoverySecureEndToEnd) { options |= DirectWebRequestOptions.RequireSsl; } - var response = requestHandler.GetResponse(request, options); - if (response.Status == HttpStatusCode.OK) { - Logger.Yadis.InfoFormat("Found host-meta for {0} at: {1}", identifier.Uri.Host, hostMetaLocation); - signingHost = hostMetaProxy.GetSigningHost(identifier); - return response; - } else { - Logger.Yadis.InfoFormat("Could not obtain host-meta for {0} from {1}", identifier.Uri.Host, hostMetaLocation); + var response = requestHandler.GetResponse(request, options).GetSnapshot(Yadis.MaximumResultToScan); + try { + if (response.Status == HttpStatusCode.OK) { + Logger.Yadis.InfoFormat("Found host-meta for {0} at: {1}", identifier.Uri.Host, hostMetaLocation); + signingHost = hostMetaProxy.GetSigningHost(identifier); + return response; + } else { + Logger.Yadis.InfoFormat("Could not obtain host-meta for {0} from {1}", identifier.Uri.Host, hostMetaLocation); + response.Dispose(); + } + } catch { + response.Dispose(); + throw; } } diff --git a/src/DotNetOpenAuth/OpenId/Identifier.cs b/src/DotNetOpenAuth/OpenId/Identifier.cs index 2ab5360..36ec784 100644 --- a/src/DotNetOpenAuth/OpenId/Identifier.cs +++ b/src/DotNetOpenAuth/OpenId/Identifier.cs @@ -39,6 +39,18 @@ namespace DotNetOpenAuth.OpenId { public string OriginalString { get; private set; } /// <summary> + /// Gets the Identifier in the form in which it should be serialized. + /// </summary> + /// <value> + /// For Identifiers that were originally deserialized, this is the exact same + /// string that was deserialized. For Identifiers instantiated in some other way, this is + /// the normalized form of the string used to instantiate the identifier. + /// </value> + internal virtual string SerializedString { + get { return this.IsDeserializedInstance ? this.OriginalString : this.ToString(); } + } + + /// <summary> /// Gets or sets a value indicating whether <see cref="Identifier"/> instances are considered equal /// based solely on their string reprsentations. /// </summary> @@ -59,6 +71,18 @@ namespace DotNetOpenAuth.OpenId { protected internal bool IsDiscoverySecureEndToEnd { get; private set; } /// <summary> + /// Gets a value indicating whether this instance was initialized from + /// deserializing a message. + /// </summary> + /// <remarks> + /// This is interesting because when an Identifier comes from the network, + /// we can't normalize it and then expect signatures to still verify. + /// But if the Identifier is initialized locally, we can and should normalize it + /// before serializing it. + /// </remarks> + protected bool IsDeserializedInstance { get; private set; } + + /// <summary> /// Converts the string representation of an Identifier to its strong type. /// </summary> /// <param name="identifier">The identifier.</param> @@ -118,11 +142,32 @@ namespace DotNetOpenAuth.OpenId { Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(identifier)); Contract.Ensures(Contract.Result<Identifier>() != null); + return Parse(identifier, false); + } + + /// <summary> + /// Parses an identifier string and automatically determines + /// whether it is an XRI or URI. + /// </summary> + /// <param name="identifier">Either a URI or XRI identifier.</param> + /// <param name="serializeExactValue">if set to <c>true</c> this Identifier will serialize exactly as given rather than in its normalized form.</param> + /// <returns> + /// An <see cref="Identifier"/> instance for the given value. + /// </returns> + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Some of these identifiers are not properly formatted to be Uris at this stage.")] + public static Identifier Parse(string identifier, bool serializeExactValue) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(identifier)); + Contract.Ensures(Contract.Result<Identifier>() != null); + + Identifier id; if (XriIdentifier.IsValidXri(identifier)) { - return new XriIdentifier(identifier); + id = new XriIdentifier(identifier); } else { - return new UriIdentifier(identifier); + id = new UriIdentifier(identifier); } + + id.IsDeserializedInstance = serializeExactValue; + return id; } /// <summary> @@ -212,6 +257,19 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> + /// Reparses the specified identifier in order to be assured that the concrete type that + /// implements the identifier is one of the well-known ones. + /// </summary> + /// <param name="identifier">The identifier.</param> + /// <returns>Either <see cref="XriIdentifier"/> or <see cref="UriIdentifier"/>.</returns> + internal static Identifier Reparse(Identifier identifier) { + Contract.Requires<ArgumentNullException>(identifier != null); + Contract.Ensures(Contract.Result<Identifier>() != null); + + return Parse(identifier, identifier.IsDeserializedInstance); + } + + /// <summary> /// Returns an <see cref="Identifier"/> that has no URI fragment. /// Quietly returns the original <see cref="Identifier"/> if it is not /// a <see cref="UriIdentifier"/> or no fragment exists. diff --git a/src/DotNetOpenAuth/OpenId/IdentifierDiscoveryResult.cs b/src/DotNetOpenAuth/OpenId/IdentifierDiscoveryResult.cs index 3190920..c851f24 100644 --- a/src/DotNetOpenAuth/OpenId/IdentifierDiscoveryResult.cs +++ b/src/DotNetOpenAuth/OpenId/IdentifierDiscoveryResult.cs @@ -89,7 +89,7 @@ namespace DotNetOpenAuth.OpenId { // not a derived type that will override expected behavior. // Elsewhere in this class, we count on the fact that this property // is either UriIdentifier or XriIdentifier. MockIdentifier messes it up. - this.claimedIdentifier = value != null ? Identifier.Parse(value) : null; + this.claimedIdentifier = value != null ? Identifier.Reparse(value) : null; } } diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs index 29315bb..43283ac 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30104.0 +// Runtime Version:4.0.30319.1 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -196,6 +196,15 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> + /// Looks up a localized string similar to This OpenID exploits features that this relying party cannot reliably verify. Please try logging in with a human-readable OpenID or from a different OpenID Provider.. + /// </summary> + internal static string ClaimedIdentifierDefiesDotNetNormalization { + get { + return ResourceManager.GetString("ClaimedIdentifierDefiesDotNetNormalization", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to The ClaimedIdentifier property must be set first.. /// </summary> internal static string ClaimedIdentifierMustBeSetFirst { @@ -416,6 +425,15 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> + /// Looks up a localized string similar to Missing {0} element.. + /// </summary> + internal static string MissingElement { + get { + return ResourceManager.GetString("MissingElement", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to No recognized association type matches the requested length of {0}.. /// </summary> internal static string NoAssociationTypeFoundByLength { diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx index ae68fe6..fab03a9 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx @@ -349,4 +349,10 @@ Discovered endpoint info: <data name="X509CertificateNotTrusted" xml:space="preserve"> <value>The X.509 certificate used to sign this document is not trusted.</value> </data> + <data name="ClaimedIdentifierDefiesDotNetNormalization" xml:space="preserve"> + <value>This OpenID exploits features that this relying party cannot reliably verify. Please try logging in with a human-readable OpenID or from a different OpenID Provider.</value> + </data> + <data name="MissingElement" xml:space="preserve"> + <value>Missing {0} element.</value> + </data> </root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OpenId/Provider/ProviderEndpoint.cs b/src/DotNetOpenAuth/OpenId/Provider/ProviderEndpoint.cs index 445978e..e792a81 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/ProviderEndpoint.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/ProviderEndpoint.cs @@ -98,11 +98,15 @@ namespace DotNetOpenAuth.OpenId.Provider { /// </remarks> public static IAuthenticationRequest PendingAuthenticationRequest { get { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); Contract.Ensures(Contract.Result<IAuthenticationRequest>() == null || PendingRequest != null); return HttpContext.Current.Session[PendingRequestKey] as IAuthenticationRequest; } set { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); HttpContext.Current.Session[PendingRequestKey] = value; } } @@ -118,11 +122,15 @@ namespace DotNetOpenAuth.OpenId.Provider { /// </remarks> public static IAnonymousRequest PendingAnonymousRequest { get { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); Contract.Ensures(Contract.Result<IAnonymousRequest>() == null || PendingRequest != null); return HttpContext.Current.Session[PendingRequestKey] as IAnonymousRequest; } set { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); HttpContext.Current.Session[PendingRequestKey] = value; } } @@ -137,8 +145,17 @@ namespace DotNetOpenAuth.OpenId.Provider { /// before responding to the relying party's request. /// </remarks> public static IHostProcessedRequest PendingRequest { - get { return HttpContext.Current.Session[PendingRequestKey] as IHostProcessedRequest; } - set { HttpContext.Current.Session[PendingRequestKey] = value; } + get { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); + return HttpContext.Current.Session[PendingRequestKey] as IHostProcessedRequest; + } + + set { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); + HttpContext.Current.Session[PendingRequestKey] = value; + } } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs index c13c61c..4aa78a5 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs @@ -702,79 +702,104 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // top row, left cell cell = new TableCell(); - this.label = new HtmlGenericControl("label"); - this.label.InnerText = LabelTextDefault; - cell.Controls.Add(this.label); - row1.Cells.Add(cell); + try { + this.label = new HtmlGenericControl("label"); + this.label.InnerText = LabelTextDefault; + cell.Controls.Add(this.label); + row1.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // top row, middle cell cell = new TableCell(); - cell.Controls.Add(new InPlaceControl(this)); - row1.Cells.Add(cell); + try { + cell.Controls.Add(new InPlaceControl(this)); + row1.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // top row, right cell cell = new TableCell(); - this.loginButton = new Button(); - this.loginButton.ID = "loginButton"; - this.loginButton.Text = ButtonTextDefault; - this.loginButton.ToolTip = ButtonToolTipDefault; - this.loginButton.Click += this.LoginButton_Click; - this.loginButton.ValidationGroup = ValidationGroupDefault; + try { + this.loginButton = new Button(); + this.loginButton.ID = "loginButton"; + this.loginButton.Text = ButtonTextDefault; + this.loginButton.ToolTip = ButtonToolTipDefault; + this.loginButton.Click += this.LoginButton_Click; + this.loginButton.ValidationGroup = ValidationGroupDefault; #if !Mono - this.panel.DefaultButton = this.loginButton.ID; + this.panel.DefaultButton = this.loginButton.ID; #endif - cell.Controls.Add(this.loginButton); - row1.Cells.Add(cell); + cell.Controls.Add(this.loginButton); + row1.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // middle row, left cell row2.Cells.Add(new TableCell()); // middle row, middle cell cell = new TableCell(); - cell.Style[HtmlTextWriterStyle.Color] = "gray"; - cell.Style[HtmlTextWriterStyle.FontSize] = "smaller"; - this.requiredValidator = new RequiredFieldValidator(); - this.requiredValidator.ErrorMessage = RequiredTextDefault + RequiredTextSuffix; - this.requiredValidator.Text = RequiredTextDefault + RequiredTextSuffix; - this.requiredValidator.Display = ValidatorDisplay.Dynamic; - this.requiredValidator.ValidationGroup = ValidationGroupDefault; - cell.Controls.Add(this.requiredValidator); - this.identifierFormatValidator = new CustomValidator(); - this.identifierFormatValidator.ErrorMessage = UriFormatTextDefault + RequiredTextSuffix; - this.identifierFormatValidator.Text = UriFormatTextDefault + RequiredTextSuffix; - this.identifierFormatValidator.ServerValidate += this.IdentifierFormatValidator_ServerValidate; - this.identifierFormatValidator.Enabled = UriValidatorEnabledDefault; - this.identifierFormatValidator.Display = ValidatorDisplay.Dynamic; - this.identifierFormatValidator.ValidationGroup = ValidationGroupDefault; - cell.Controls.Add(this.identifierFormatValidator); - this.errorLabel = new Label(); - this.errorLabel.EnableViewState = false; - this.errorLabel.ForeColor = System.Drawing.Color.Red; - this.errorLabel.Style[HtmlTextWriterStyle.Display] = "block"; // puts it on its own line - this.errorLabel.Visible = false; - cell.Controls.Add(this.errorLabel); - this.examplePrefixLabel = new Label(); - this.examplePrefixLabel.Text = ExamplePrefixDefault; - cell.Controls.Add(this.examplePrefixLabel); - cell.Controls.Add(new LiteralControl(" ")); - this.exampleUrlLabel = new Label(); - this.exampleUrlLabel.Font.Bold = true; - this.exampleUrlLabel.Text = ExampleUrlDefault; - cell.Controls.Add(this.exampleUrlLabel); - row2.Cells.Add(cell); + try { + cell.Style[HtmlTextWriterStyle.Color] = "gray"; + cell.Style[HtmlTextWriterStyle.FontSize] = "smaller"; + this.requiredValidator = new RequiredFieldValidator(); + this.requiredValidator.ErrorMessage = RequiredTextDefault + RequiredTextSuffix; + this.requiredValidator.Text = RequiredTextDefault + RequiredTextSuffix; + this.requiredValidator.Display = ValidatorDisplay.Dynamic; + this.requiredValidator.ValidationGroup = ValidationGroupDefault; + cell.Controls.Add(this.requiredValidator); + this.identifierFormatValidator = new CustomValidator(); + this.identifierFormatValidator.ErrorMessage = UriFormatTextDefault + RequiredTextSuffix; + this.identifierFormatValidator.Text = UriFormatTextDefault + RequiredTextSuffix; + this.identifierFormatValidator.ServerValidate += this.IdentifierFormatValidator_ServerValidate; + this.identifierFormatValidator.Enabled = UriValidatorEnabledDefault; + this.identifierFormatValidator.Display = ValidatorDisplay.Dynamic; + this.identifierFormatValidator.ValidationGroup = ValidationGroupDefault; + cell.Controls.Add(this.identifierFormatValidator); + this.errorLabel = new Label(); + this.errorLabel.EnableViewState = false; + this.errorLabel.ForeColor = System.Drawing.Color.Red; + this.errorLabel.Style[HtmlTextWriterStyle.Display] = "block"; // puts it on its own line + this.errorLabel.Visible = false; + cell.Controls.Add(this.errorLabel); + this.examplePrefixLabel = new Label(); + this.examplePrefixLabel.Text = ExamplePrefixDefault; + cell.Controls.Add(this.examplePrefixLabel); + cell.Controls.Add(new LiteralControl(" ")); + this.exampleUrlLabel = new Label(); + this.exampleUrlLabel.Font.Bold = true; + this.exampleUrlLabel.Text = ExampleUrlDefault; + cell.Controls.Add(this.exampleUrlLabel); + row2.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // middle row, right cell cell = new TableCell(); - cell.Style[HtmlTextWriterStyle.Color] = "gray"; - cell.Style[HtmlTextWriterStyle.FontSize] = "smaller"; - cell.Style[HtmlTextWriterStyle.TextAlign] = "center"; - this.registerLink = new HyperLink(); - this.registerLink.Text = RegisterTextDefault; - this.registerLink.ToolTip = RegisterToolTipDefault; - this.registerLink.NavigateUrl = RegisterUrlDefault; - this.registerLink.Visible = RegisterVisibleDefault; - cell.Controls.Add(this.registerLink); - row2.Cells.Add(cell); + try { + cell.Style[HtmlTextWriterStyle.Color] = "gray"; + cell.Style[HtmlTextWriterStyle.FontSize] = "smaller"; + cell.Style[HtmlTextWriterStyle.TextAlign] = "center"; + this.registerLink = new HyperLink(); + this.registerLink.Text = RegisterTextDefault; + this.registerLink.ToolTip = RegisterToolTipDefault; + this.registerLink.NavigateUrl = RegisterUrlDefault; + this.registerLink.Visible = RegisterVisibleDefault; + cell.Controls.Add(this.registerLink); + row2.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // bottom row, left cell cell = new TableCell(); @@ -782,17 +807,27 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // bottom row, middle cell cell = new TableCell(); - this.rememberMeCheckBox = new CheckBox(); - this.rememberMeCheckBox.Text = RememberMeTextDefault; - this.rememberMeCheckBox.Checked = this.UsePersistentCookie != LogOnPersistence.Session; - this.rememberMeCheckBox.Visible = RememberMeVisibleDefault; - this.rememberMeCheckBox.CheckedChanged += this.RememberMeCheckBox_CheckedChanged; - cell.Controls.Add(this.rememberMeCheckBox); - row3.Cells.Add(cell); + try { + this.rememberMeCheckBox = new CheckBox(); + this.rememberMeCheckBox.Text = RememberMeTextDefault; + this.rememberMeCheckBox.Checked = this.UsePersistentCookie != LogOnPersistence.Session; + this.rememberMeCheckBox.Visible = RememberMeVisibleDefault; + this.rememberMeCheckBox.CheckedChanged += this.RememberMeCheckBox_CheckedChanged; + cell.Controls.Add(this.rememberMeCheckBox); + row3.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // bottom row, right cell cell = new TableCell(); - row3.Cells.Add(cell); + try { + row3.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // this sets all the controls' tab indexes this.TabIndex = TabIndexDefault; diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs index dbf9530..8684bd1 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs @@ -762,13 +762,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { IRelyingPartyApplicationStore store = this.Stateless ? null : (this.CustomApplicationStore ?? DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore)); var rp = new OpenIdRelyingParty(store); - - // Only set RequireSsl to true, as we don't want to override - // a .config setting of true with false. - if (this.RequireSsl) { - rp.SecuritySettings.RequireSsl = true; + try { + // Only set RequireSsl to true, as we don't want to override + // a .config setting of true with false. + if (this.RequireSsl) { + rp.SecuritySettings.RequireSsl = true; + } + return rp; + } catch { + rp.Dispose(); + throw; } - return rp; } } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs index 37ba8c1..f22645f 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs @@ -321,7 +321,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <param name="identifiers">The identifiers to perform discovery on.</param> protected void PreloadDiscovery(IEnumerable<Identifier> identifiers) { string script = this.AjaxRelyingParty.AsAjaxPreloadedDiscoveryResult( - identifiers.Select(id => this.CreateRequests(id)).Flatten()); + identifiers.SelectMany(id => this.CreateRequests(id))); this.Page.ClientScript.RegisterClientScriptBlock(typeof(OpenIdRelyingPartyAjaxControlBase), this.ClientID, script, true); } @@ -399,6 +399,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param> protected override void Render(HtmlTextWriter writer) { + Contract.Assume(writer != null, "Missing contract."); base.Render(writer); // Emit a hidden field to let the javascript on the user agent know if an diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs index 838b749..5090ecd 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs @@ -358,6 +358,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } set { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(value)); + if (Page != null && !DesignMode) { // Validate new value by trying to construct a Realm object based on it. new Realm(OpenIdUtilities.GetResolvedRealm(this.Page, value, this.RelyingParty.Channel.GetRequestFromContext())); // throws an exception on failure. diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs index 5b85e7c..b7a54eb 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs @@ -310,11 +310,16 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.EnsureValidButtons(); var css = new HtmlLink(); - css.Href = this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedStylesheetResourceName); - css.Attributes["rel"] = "stylesheet"; - css.Attributes["type"] = "text/css"; - ErrorUtilities.VerifyHost(this.Page.Header != null, OpenIdStrings.HeadTagMustIncludeRunatServer); - this.Page.Header.Controls.AddAt(0, css); // insert at top so host page can override + try { + css.Href = this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedStylesheetResourceName); + css.Attributes["rel"] = "stylesheet"; + css.Attributes["type"] = "text/css"; + ErrorUtilities.VerifyHost(this.Page.Header != null, OpenIdStrings.HeadTagMustIncludeRunatServer); + this.Page.Header.Controls.AddAt(0, css); // insert at top so host page can override + } catch { + css.Dispose(); + throw; + } // Import the .js file where most of the code is. this.Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdSelector), EmbeddedScriptResourceName); @@ -344,6 +349,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param> protected override void Render(HtmlTextWriter writer) { + Contract.Assume(writer != null, "Missing contract"); writer.AddAttribute(HtmlTextWriterAttribute.Class, "OpenIdProviders"); writer.RenderBeginTag(HtmlTextWriterTag.Ul); diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs index 08e7aac..335b435 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs @@ -584,6 +584,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param> protected override void Render(HtmlTextWriter writer) { + Contract.Assume(writer != null, "Missing contract."); + if (this.ShowLogo) { string logoUrl = Page.ClientScript.GetWebResourceUrl( typeof(OpenIdTextBox), EmbeddedLogoResourceName); @@ -625,6 +627,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// true if the server control's state changes as a result of the postback; otherwise, false. /// </returns> protected virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection) { + Contract.Assume(postCollection != null, "Missing contract"); + // If the control was temporarily hidden, it won't be in the Form data, // and we'll just implicitly keep the last Text setting. if (postCollection[this.Name] != null) { diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs index b6a1b76..3e2298c 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs @@ -146,6 +146,14 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } } + // Check whether this particular identifier presents a problem with HTTP discovery + // due to limitations in the .NET Uri class. + UriIdentifier claimedIdUri = claimedId as UriIdentifier; + if (claimedIdUri != null && claimedIdUri.ProblematicNormalization) { + ErrorUtilities.VerifyProtocol(relyingParty.SecuritySettings.AllowApproximateIdentifierDiscovery, OpenIdStrings.ClaimedIdentifierDefiesDotNetNormalization); + Logger.OpenId.WarnFormat("Positive assertion for claimed identifier {0} cannot be precisely verified under partial trust hosting due to .NET limitation. An approximate verification will be attempted.", claimedId); + } + // While it LOOKS like we're performing discovery over HTTP again // Yadis.IdentifierDiscoveryCachePolicy is set to HttpRequestCacheLevel.CacheIfAvailable // which means that the .NET runtime is caching our discoveries for us. This turns out diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartySecuritySettings.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartySecuritySettings.cs index e2bf2a1..a7686c5 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartySecuritySettings.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartySecuritySettings.cs @@ -27,6 +27,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { : base(false) { this.PrivateSecretMaximumAge = TimeSpan.FromDays(7); this.ProtectDownlevelReplayAttacks = ProtectDownlevelReplayAttacksDefault; + this.AllowApproximateIdentifierDiscovery = true; } /// <summary> @@ -132,6 +133,16 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { public bool AllowDualPurposeIdentifiers { get; set; } /// <summary> + /// Gets or sets a value indicating whether certain Claimed Identifiers that exploit + /// features that .NET does not have the ability to send exact HTTP requests for will + /// still be allowed by using an approximate HTTP request. + /// </summary> + /// <value> + /// The default value is <c>true</c>. + /// </value> + public bool AllowApproximateIdentifierDiscovery { get; set; } + + /// <summary> /// Gets or sets a value indicating whether special measures are taken to /// protect users from replay attacks when those users' identities are hosted /// by OpenID 1.x Providers. diff --git a/src/DotNetOpenAuth/OpenId/UriIdentifier.cs b/src/DotNetOpenAuth/OpenId/UriIdentifier.cs index 0b7a0e3..278ae37 100644 --- a/src/DotNetOpenAuth/OpenId/UriIdentifier.cs +++ b/src/DotNetOpenAuth/OpenId/UriIdentifier.cs @@ -10,6 +10,9 @@ namespace DotNetOpenAuth.OpenId { using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; + using System.Reflection; + using System.Security; + using System.Text; using System.Text.RegularExpressions; using System.Web.UI.HtmlControls; using System.Xml; @@ -30,6 +33,68 @@ namespace DotNetOpenAuth.OpenId { private static readonly string[] allowedSchemes = { "http", "https" }; /// <summary> + /// The special scheme to use for HTTP URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser roundTrippingHttpParser = new NonPathCompressingUriParser(Uri.UriSchemeHttp); + + /// <summary> + /// The special scheme to use for HTTPS URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser roundTrippingHttpsParser = new NonPathCompressingUriParser(Uri.UriSchemeHttps); + + /// <summary> + /// The special scheme to use for HTTP URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser publishableHttpParser = new NonPathCompressingUriParser(Uri.UriSchemeHttp); + + /// <summary> + /// The special scheme to use for HTTPS URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser publishableHttpsParser = new NonPathCompressingUriParser(Uri.UriSchemeHttps); + + /// <summary> + /// A value indicating whether scheme substitution is being used to workaround + /// .NET path compression that invalidates some OpenIDs that have trailing periods + /// in one of their path segments. + /// </summary> + private static bool schemeSubstitution; + + /// <summary> + /// Initializes static members of the <see cref="UriIdentifier"/> class. + /// </summary> + /// <remarks> + /// This method attempts to workaround the .NET Uri class parsing bug described here: + /// https://connect.microsoft.com/VisualStudio/feedback/details/386695/system-uri-incorrectly-strips-trailing-dots?wa=wsignin1.0#tabs + /// since some identifiers (like some of the pseudonymous identifiers from Yahoo) include path segments + /// that end with periods, which the Uri class will typically trim off. + /// </remarks> + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Some things just can't be done in a field initializer.")] + static UriIdentifier() { + // Our first attempt to handle trailing periods in path segments is to leverage + // full trust if it's available to rewrite the rules. + // In fact this is the ONLY way in .NET 3.5 (and arguably in .NET 4.0) to send + // outbound HTTP requests with trailing periods, so it's the only way to perform + // discovery on such an identifier. + try { + UriParser.Register(roundTrippingHttpParser, "dnoarthttp", 80); + UriParser.Register(roundTrippingHttpsParser, "dnoarthttps", 443); + UriParser.Register(publishableHttpParser, "dnoahttp", 80); + UriParser.Register(publishableHttpsParser, "dnoahttps", 443); + roundTrippingHttpParser.Initialize(false); + roundTrippingHttpsParser.Initialize(false); + publishableHttpParser.Initialize(true); + publishableHttpsParser.Initialize(true); + schemeSubstitution = true; + Logger.OpenId.Debug(".NET Uri class path compression overridden."); + Reporting.RecordFeatureUse("FullTrust"); + } catch (SecurityException) { + // We must be running in partial trust. Nothing more we can do. + Logger.OpenId.Warn("Unable to coerce .NET to stop compressing URI paths due to partial trust limitations. Some URL identifiers may be unable to complete login."); + Reporting.RecordFeatureUse("PartialTrust"); + } + } + + /// <summary> /// Initializes a new instance of the <see cref="UriIdentifier"/> class. /// </summary> /// <param name="uri">The value this identifier will represent.</param> @@ -62,7 +127,8 @@ namespace DotNetOpenAuth.OpenId { /// Initializes a new instance of the <see cref="UriIdentifier"/> class. /// </summary> /// <param name="uri">The value this identifier will represent.</param> - internal UriIdentifier(Uri uri) : this(uri, false) { + internal UriIdentifier(Uri uri) + : this(uri, false) { } /// <summary> @@ -73,7 +139,13 @@ namespace DotNetOpenAuth.OpenId { internal UriIdentifier(Uri uri, bool requireSslDiscovery) : base(uri != null ? uri.OriginalString : null, requireSslDiscovery) { Contract.Requires<ArgumentNullException>(uri != null); - if (!TryCanonicalize(new UriBuilder(uri), out uri)) { + + string uriAsString = uri.OriginalString; + if (schemeSubstitution) { + uriAsString = NormalSchemeToSpecialRoundTrippingScheme(uriAsString); + } + + if (!TryCanonicalize(uriAsString, out uri)) { throw new UriFormatException(); } if (requireSslDiscovery && uri.Scheme != Uri.UriSchemeHttps) { @@ -96,6 +168,26 @@ namespace DotNetOpenAuth.OpenId { internal bool SchemeImplicitlyPrepended { get; private set; } /// <summary> + /// Gets a value indicating whether this Identifier has characters or patterns that + /// the <see cref="Uri"/> class normalizes away and invalidating the Identifier. + /// </summary> + internal bool ProblematicNormalization { + get { + if (schemeSubstitution) { + // With full trust, we have no problematic URIs + return false; + } + + var simpleUri = new SimpleUri(this.OriginalString); + if (simpleUri.Path.EndsWith(".", StringComparison.Ordinal) || simpleUri.Path.Contains("./")) { + return true; + } + + return false; + } + } + + /// <summary> /// Converts a <see cref="UriIdentifier"/> instance to a <see cref="Uri"/> instance. /// </summary> /// <param name="identifier">The identifier to convert to an ordinary <see cref="Uri"/> instance.</param> @@ -137,7 +229,12 @@ namespace DotNetOpenAuth.OpenId { if (other == null) { return false; } - return this.Uri == other.Uri; + + if (this.ProblematicNormalization || other.ProblematicNormalization) { + return new SimpleUri(this.OriginalString).Equals(new SimpleUri(other.OriginalString)); + } else { + return this.Uri == other.Uri; + } } /// <summary> @@ -157,16 +254,13 @@ namespace DotNetOpenAuth.OpenId { /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. /// </returns> public override string ToString() { - return Uri.AbsoluteUri; - } -#if UNUSED - static bool TryCanonicalize(string uri, out string canonicalUri) { - Uri normalizedUri; - bool result = TryCanonicalize(uri, out normalizedUri); - canonicalUri = normalizedUri.AbsoluteUri; - return result; + if (this.ProblematicNormalization) { + return new SimpleUri(this.OriginalString).ToString(); + } else { + return this.Uri.AbsoluteUri; + } } -#endif + /// <summary> /// Determines whether a URI is a valid OpenID Identifier (of any kind). /// </summary> @@ -222,9 +316,7 @@ namespace DotNetOpenAuth.OpenId { } // Strip the fragment. - UriBuilder builder = new UriBuilder(Uri); - builder.Fragment = null; - return builder.Uri; + return new UriIdentifier(this.OriginalString.Substring(0, this.OriginalString.IndexOf('#'))); } /// <summary> @@ -335,8 +427,12 @@ namespace DotNetOpenAuth.OpenId { schemePrepended = true; } + if (schemeSubstitution) { + uri = NormalSchemeToSpecialRoundTrippingScheme(uri); + } + // Use a UriBuilder because it helps to normalize the URL as well. - return TryCanonicalize(new UriBuilder(uri), out canonicalUri); + return TryCanonicalize(uri, out canonicalUri); } catch (UriFormatException) { // We try not to land here with checks in the try block, but just in case. return false; @@ -344,9 +440,9 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> - /// Removes the fragment from a URL and sets the host to lowercase. + /// Fixes up the scheme if appropriate. /// </summary> - /// <param name="uriBuilder">The URI builder with the value to canonicalize.</param> + /// <param name="uri">The URI to canonicalize.</param> /// <param name="canonicalUri">The resulting canonical URI.</param> /// <returns><c>true</c> if the canonicalization was successful; <c>false</c> otherwise.</returns> /// <remarks> @@ -356,12 +452,48 @@ namespace DotNetOpenAuth.OpenId { /// For this, you should lookup the value stored in IAuthenticationResponse.ClaimedIdentifier. /// </remarks> [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "The user will see the result of this operation and they want to see it in lower case.")] - private static bool TryCanonicalize(UriBuilder uriBuilder, out Uri canonicalUri) { - uriBuilder.Host = uriBuilder.Host.ToLowerInvariant(); - canonicalUri = uriBuilder.Uri; + private static bool TryCanonicalize(string uri, out Uri canonicalUri) { + Contract.Requires<ArgumentNullException>(uri != null); + + if (schemeSubstitution) { + UriBuilder uriBuilder = new UriBuilder(uri); + + // Swap out our round-trippable scheme for the publishable (hidden) scheme. + uriBuilder.Scheme = uriBuilder.Scheme == roundTrippingHttpParser.RegisteredScheme ? publishableHttpParser.RegisteredScheme : publishableHttpsParser.RegisteredScheme; + canonicalUri = uriBuilder.Uri; + } else { + canonicalUri = new Uri(uri); + } + return true; } + /// <summary> + /// Gets the special non-compressing scheme or URL for a standard scheme or URL. + /// </summary> + /// <param name="normal">The ordinary URL or scheme name.</param> + /// <returns>The non-compressing equivalent scheme or URL for the given value.</returns> + private static string NormalSchemeToSpecialRoundTrippingScheme(string normal) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(normal)); + Contract.Requires<InternalErrorException>(schemeSubstitution); + Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>())); + + int delimiterIndex = normal.IndexOf(Uri.SchemeDelimiter); + string normalScheme = delimiterIndex < 0 ? normal : normal.Substring(0, delimiterIndex); + string nonCompressingScheme; + if (string.Equals(normalScheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || + string.Equals(normalScheme, publishableHttpParser.RegisteredScheme, StringComparison.OrdinalIgnoreCase)) { + nonCompressingScheme = roundTrippingHttpParser.RegisteredScheme; + } else if (string.Equals(normalScheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) || + string.Equals(normalScheme, publishableHttpsParser.RegisteredScheme, StringComparison.OrdinalIgnoreCase)) { + nonCompressingScheme = roundTrippingHttpsParser.RegisteredScheme; + } else { + throw new NotSupportedException(); + } + + return delimiterIndex < 0 ? nonCompressingScheme : nonCompressingScheme + normal.Substring(delimiterIndex); + } + #if CONTRACTS_FULL /// <summary> /// Verifies conditions that should be true for any valid state of this object. @@ -374,5 +506,178 @@ namespace DotNetOpenAuth.OpenId { Contract.Invariant(this.Uri.AbsoluteUri != null); } #endif + + /// <summary> + /// A simple URI class that doesn't suffer from the parsing problems of the <see cref="Uri"/> class. + /// </summary> + internal class SimpleUri { + /// <summary> + /// URI characters that separate the URI Path from subsequent elements. + /// </summary> + private static readonly char[] PathEndingCharacters = new char[] { '?', '#' }; + + /// <summary> + /// Initializes a new instance of the <see cref="SimpleUri"/> class. + /// </summary> + /// <param name="value">The value.</param> + internal SimpleUri(string value) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(value)); + + // Leverage the Uri class's parsing where we can. + Uri uri = new Uri(value); + this.Scheme = uri.Scheme; + this.Authority = uri.Authority; + this.Query = uri.Query; + this.Fragment = uri.Fragment; + + // Get the Path out ourselves, since the default Uri parser compresses it too much for OpenID. + int schemeLength = value.IndexOf(Uri.SchemeDelimiter, StringComparison.Ordinal); + Contract.Assume(schemeLength > 0); + int hostStart = schemeLength + Uri.SchemeDelimiter.Length; + int hostFinish = value.IndexOf('/', hostStart); + if (hostFinish < 0) { + this.Path = "/"; + } else { + int pathFinish = value.IndexOfAny(PathEndingCharacters, hostFinish); + Contract.Assume(pathFinish >= hostFinish || pathFinish < 0); + if (pathFinish < 0) { + this.Path = value.Substring(hostFinish); + } else { + this.Path = value.Substring(hostFinish, pathFinish - hostFinish); + } + } + + this.Path = NormalizePathEscaping(this.Path); + } + + /// <summary> + /// Gets the scheme. + /// </summary> + /// <value>The scheme.</value> + public string Scheme { get; private set; } + + /// <summary> + /// Gets the authority. + /// </summary> + /// <value>The authority.</value> + public string Authority { get; private set; } + + /// <summary> + /// Gets the path of the URI. + /// </summary> + /// <value>The path from the URI.</value> + public string Path { get; private set; } + + /// <summary> + /// Gets the query. + /// </summary> + /// <value>The query.</value> + public string Query { get; private set; } + + /// <summary> + /// Gets the fragment. + /// </summary> + /// <value>The fragment.</value> + public string Fragment { get; private set; } + + /// <summary> + /// Returns a <see cref="System.String"/> that represents this instance. + /// </summary> + /// <returns> + /// A <see cref="System.String"/> that represents this instance. + /// </returns> + public override string ToString() { + return this.Scheme + Uri.SchemeDelimiter + this.Authority + this.Path + this.Query + this.Fragment; + } + + /// <summary> + /// Determines whether the specified <see cref="System.Object"/> is equal to this instance. + /// </summary> + /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param> + /// <returns> + /// <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>. + /// </returns> + /// <exception cref="T:System.NullReferenceException"> + /// The <paramref name="obj"/> parameter is null. + /// </exception> + public override bool Equals(object obj) { + SimpleUri other = obj as SimpleUri; + if (other == null) { + return false; + } + + // Note that this equality check is intentionally leaving off the Fragment part + // to match Uri behavior, and is intentionally being case sensitive and insensitive + // for different parts. + return string.Equals(this.Scheme, other.Scheme, StringComparison.OrdinalIgnoreCase) && + string.Equals(this.Authority, other.Authority, StringComparison.OrdinalIgnoreCase) && + string.Equals(this.Path, other.Path, StringComparison.Ordinal) && + string.Equals(this.Query, other.Query, StringComparison.Ordinal); + } + + /// <summary> + /// Normalizes the characters that are escaped in the given URI path. + /// </summary> + /// <param name="path">The path to normalize.</param> + /// <returns>The given path, with exactly those characters escaped which should be.</returns> + private static string NormalizePathEscaping(string path) { + Contract.Requires<ArgumentNullException>(path != null); + + string[] segments = path.Split('/'); + for (int i = 0; i < segments.Length; i++) { + segments[i] = Uri.EscapeDataString(Uri.UnescapeDataString(segments[i])); + } + + return string.Join("/", segments); + } + } + + /// <summary> + /// A URI parser that does not compress paths, such as trimming trailing periods from path segments. + /// </summary> + private class NonPathCompressingUriParser : GenericUriParser { + /// <summary> + /// The field that stores the scheme that this parser is registered under. + /// </summary> + private static FieldInfo schemeField; + + /// <summary> + /// The standard "http" or "https" scheme that this parser is subverting. + /// </summary> + private string standardScheme; + + /// <summary> + /// Initializes a new instance of the <see cref="NonPathCompressingUriParser"/> class. + /// </summary> + /// <param name="standardScheme">The standard scheme that this parser will be subverting.</param> + public NonPathCompressingUriParser(string standardScheme) + : base(GenericUriParserOptions.DontCompressPath | GenericUriParserOptions.IriParsing | GenericUriParserOptions.Idn) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(standardScheme)); + this.standardScheme = standardScheme; + } + + /// <summary> + /// Gets the scheme this parser is registered under. + /// </summary> + /// <value>The registered scheme.</value> + internal string RegisteredScheme { get; private set; } + + /// <summary> + /// Initializes this parser with the actual scheme it should appear to be. + /// </summary> + /// <param name="hideNonStandardScheme">if set to <c>true</c> Uris using this scheme will look like they're using the original standard scheme.</param> + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Schemes are traditionally displayed in lowercase.")] + internal void Initialize(bool hideNonStandardScheme) { + if (schemeField == null) { + schemeField = typeof(UriParser).GetField("m_Scheme", BindingFlags.NonPublic | BindingFlags.Instance); + } + + this.RegisteredScheme = (string)schemeField.GetValue(this); + + if (hideNonStandardScheme) { + schemeField.SetValue(this, this.standardScheme.ToLowerInvariant()); + } + } + } } } diff --git a/src/DotNetOpenAuth/Reporting.cs b/src/DotNetOpenAuth/Reporting.cs index c4421c4..612845f 100644 --- a/src/DotNetOpenAuth/Reporting.cs +++ b/src/DotNetOpenAuth/Reporting.cs @@ -151,6 +151,7 @@ namespace DotNetOpenAuth { /// </summary> /// <param name="eventName">Name of the event.</param> /// <param name="category">The category within the event. Null and empty strings are allowed, but considered the same.</param> + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "PersistentCounter instances are stored in a table for later use.")] internal static void RecordEventOccurrence(string eventName, string category) { Contract.Requires(!String.IsNullOrEmpty(eventName)); @@ -318,6 +319,7 @@ namespace DotNetOpenAuth { /// <summary> /// Initializes Reporting if it has not been initialized yet. /// </summary> + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This method must never throw.")] private static void Initialize() { lock (initializationSync) { if (!broken && !initialized) { @@ -355,44 +357,49 @@ namespace DotNetOpenAuth { /// <returns>A stream that contains the report.</returns> private static Stream GetReport() { var stream = new MemoryStream(); - var writer = new StreamWriter(stream, Encoding.UTF8); - writer.WriteLine(reportOriginIdentity.ToString("B")); - writer.WriteLine(Util.LibraryVersion); - writer.WriteLine(".NET Framework {0}", Environment.Version); - - foreach (var observation in observations) { - observation.Flush(); - writer.WriteLine("===================================="); - writer.WriteLine(observation.FileName); - try { - using (var fileStream = new IsolatedStorageFileStream(observation.FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { - writer.Flush(); - fileStream.CopyTo(writer.BaseStream); + try { + var writer = new StreamWriter(stream, Encoding.UTF8); + writer.WriteLine(reportOriginIdentity.ToString("B")); + writer.WriteLine(Util.LibraryVersion); + writer.WriteLine(".NET Framework {0}", Environment.Version); + + foreach (var observation in observations) { + observation.Flush(); + writer.WriteLine("===================================="); + writer.WriteLine(observation.FileName); + try { + using (var fileStream = new IsolatedStorageFileStream(observation.FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { + writer.Flush(); + fileStream.CopyTo(writer.BaseStream); + } + } catch (FileNotFoundException) { + writer.WriteLine("(missing)"); } - } catch (FileNotFoundException) { - writer.WriteLine("(missing)"); } - } - // Not all event counters may have even loaded in this app instance. - // We flush the ones in memory, and then read all of them off disk. - foreach (var counter in events.Values) { - counter.Flush(); - } + // Not all event counters may have even loaded in this app instance. + // We flush the ones in memory, and then read all of them off disk. + foreach (var counter in events.Values) { + counter.Flush(); + } - foreach (string eventFile in file.GetFileNames("event-*.txt")) { - writer.WriteLine("===================================="); - writer.WriteLine(eventFile); - using (var fileStream = new IsolatedStorageFileStream(eventFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { - writer.Flush(); - fileStream.CopyTo(writer.BaseStream); + foreach (string eventFile in file.GetFileNames("event-*.txt")) { + writer.WriteLine("===================================="); + writer.WriteLine(eventFile); + using (var fileStream = new IsolatedStorageFileStream(eventFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { + writer.Flush(); + fileStream.CopyTo(writer.BaseStream); + } } - } - // Make sure the stream is positioned at the beginning. - writer.Flush(); - stream.Position = 0; - return stream; + // Make sure the stream is positioned at the beginning. + writer.Flush(); + stream.Position = 0; + return stream; + } catch { + stream.Dispose(); + throw; + } } /// <summary> diff --git a/src/DotNetOpenAuth/Util.cs b/src/DotNetOpenAuth/Util.cs index 9f8b30c..8a18ef8 100644 --- a/src/DotNetOpenAuth/Util.cs +++ b/src/DotNetOpenAuth/Util.cs @@ -126,7 +126,7 @@ namespace DotNetOpenAuth { sb.Append("\t"); sb.Append(objString); - if (!objString.EndsWith(Environment.NewLine)) { + if (!objString.EndsWith(Environment.NewLine, StringComparison.Ordinal)) { sb.AppendLine(); } sb.AppendLine("}, {"); diff --git a/src/DotNetOpenAuth/XrdsPublisher.cs b/src/DotNetOpenAuth/XrdsPublisher.cs index e7c04d8..83d82ff 100644 --- a/src/DotNetOpenAuth/XrdsPublisher.cs +++ b/src/DotNetOpenAuth/XrdsPublisher.cs @@ -9,6 +9,7 @@ namespace DotNetOpenAuth { using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; using System.Drawing.Design; using System.Text; using System.Web; @@ -209,6 +210,7 @@ namespace DotNetOpenAuth { /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param> [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")] protected override void Render(HtmlTextWriter writer) { + Contract.Assume(writer != null, "Missing contract."); if (this.Enabled && this.Visible && !string.IsNullOrEmpty(this.XrdsUrl)) { Uri xrdsAddress = new Uri(MessagingUtilities.GetRequestUrlFromContext(), Page.Response.ApplyAppPathModifier(this.XrdsUrl)); if ((this.XrdsAdvertisement & XrdsUrlLocations.HttpHeader) != 0) { diff --git a/src/DotNetOpenAuth/Yadis/Yadis.cs b/src/DotNetOpenAuth/Yadis/Yadis.cs index 7882797..8b8c20f 100644 --- a/src/DotNetOpenAuth/Yadis/Yadis.cs +++ b/src/DotNetOpenAuth/Yadis/Yadis.cs @@ -39,7 +39,7 @@ namespace DotNetOpenAuth.Yadis { /// The maximum number of bytes to read from an HTTP response /// in searching for a link to a YADIS document. /// </summary> - private const int MaximumResultToScan = 1024 * 1024; + internal const int MaximumResultToScan = 1024 * 1024; /// <summary> /// Performs YADIS discovery on some identifier. diff --git a/tools/DotNetOpenAuth.automated.props b/tools/DotNetOpenAuth.automated.props index f66c1fe..6037967 100644 --- a/tools/DotNetOpenAuth.automated.props +++ b/tools/DotNetOpenAuth.automated.props @@ -6,6 +6,7 @@ <AutomatedBuild>true</AutomatedBuild> <SolutionPath>$(ProjectRoot)src\$(ProductName).sln</SolutionPath> <BuildInParallel Condition=" '$(BuildInParallel)' == '' ">true</BuildInParallel> + <DowngradeMvc2ToMvc1 Condition=" '$(DowngradeMvc2ToMvc1)' == '' ">false</DowngradeMvc2ToMvc1> <!-- Validation controls whether extra builds are done in order to fully validate what we're distributing, even if we're not distributing the built bits (as is the case for project templates). --> <Validation Condition=" '$(Validation)' == '' ">Full</Validation> diff --git a/tools/DotNetOpenAuth.targets b/tools/DotNetOpenAuth.targets index cab4413..8469aa6 100644 --- a/tools/DotNetOpenAuth.targets +++ b/tools/DotNetOpenAuth.targets @@ -17,6 +17,7 @@ <PropertyGroup> <DefineConstants Condition=" '$(SignAssembly)' == 'true' ">$(DefineConstants);StrongNameSigned</DefineConstants> <DefineConstants Condition=" '$(ClrVersion)' == '4' ">$(DefineConstants);CLR4</DefineConstants> + <AssemblySearchPaths>$(AssemblySearchPaths);$(ProjectRoot)lib</AssemblySearchPaths> </PropertyGroup> <ItemGroup> diff --git a/tools/drop.proj b/tools/drop.proj index be5d3f9..317aba6 100644 --- a/tools/drop.proj +++ b/tools/drop.proj @@ -44,7 +44,7 @@ $(ILMergeOutputAssemblyDirectory)$(SignedSubPath)$(ProductName).dll; $(ILMergeOutputAssemblyDirectory)$(ProductName).pdb; $(OutputPath)$(ProductName).xml; - $(OutputPath)$(SignedSubPath)$(ProductName).Contracts.???; + $(OutputPath)CodeContracts\$(SignedSubPath)$(ProductName).Contracts.???; $(ProjectRoot)Doc\README.Bin.html; $(ProjectRoot)src\$(ProductName)\Configuration\$(ProductName).xsd; " /> @@ -133,7 +133,10 @@ AddReferences="Microsoft.Contracts"/> <ChangeProjectReferenceToAssemblyReference Projects="@(SampleProjectTargets)" ProjectReferences="..\..\src\$(ProductName)\$(ProductName).csproj" References="..\..\Bin\$(ProductName).dll" /> - <DowngradeProjects Projects="@(SampleProjectTargets);@(SampleSolutionTargets)" DowngradeMvc2ToMvc1="true" /> + <DowngradeProjects + Projects="@(SampleProjectTargets);@(SampleSolutionTargets)" + DowngradeMvc2ToMvc1="$(DowngradeMvc2ToMvc1)" + /> </Target> <Target Name="Build" DependsOnTargets="Layout"> diff --git a/vsix/extension.vsixmanifest b/vsix/extension.vsixmanifest index 20c2472..394574c 100644 --- a/vsix/extension.vsixmanifest +++ b/vsix/extension.vsixmanifest @@ -13,12 +13,12 @@ <InstalledByMsi>false</InstalledByMsi> <SupportedProducts> <VisualStudio Version="10.0"> - <Edition>VSTS</Edition> - <Edition>VSTD</Edition> <Edition>Pro</Edition> + <Edition>Premium</Edition> + <Edition>Ultimate</Edition> </VisualStudio> </SupportedProducts> - <SupportedFrameworkRuntimeEdition MinVersion="3.5" MaxVersion="3.5" /> + <SupportedFrameworkRuntimeEdition MinVersion="3.5" MaxVersion="4.0" /> </Identifier> <References /> <Content> |