diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2010-07-14 22:05:04 -0700 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2010-07-14 22:05:04 -0700 |
commit | 9b4fc3a38cbd1b427c25b906a5562ec5586f9340 (patch) | |
tree | 40244fa2bfa767bf8bfcfbf19690a8d63fb2be05 | |
parent | 1882f59229ee85cf2b9cf66cfd2ada1cc27520f7 (diff) | |
download | DotNetOpenAuth-9b4fc3a38cbd1b427c25b906a5562ec5586f9340.zip DotNetOpenAuth-9b4fc3a38cbd1b427c25b906a5562ec5586f9340.tar.gz DotNetOpenAuth-9b4fc3a38cbd1b427c25b906a5562ec5586f9340.tar.bz2 |
Lots of work toward OAuth 2.0 in project templates and OAuthConsumerWpf sample.
The WebFormsRelyingParty now works with the sample WPF OAuth client in a modified user-agent mode.
25 files changed, 276 insertions, 176 deletions
diff --git a/projecttemplates/RelyingPartyDatabase/Schema Objects/Schemas/dbo/Tables/Client.table.sql b/projecttemplates/RelyingPartyDatabase/Schema Objects/Schemas/dbo/Tables/Client.table.sql index 7da9c5e..8dc2f64 100644 --- a/projecttemplates/RelyingPartyDatabase/Schema Objects/Schemas/dbo/Tables/Client.table.sql +++ b/projecttemplates/RelyingPartyDatabase/Schema Objects/Schemas/dbo/Tables/Client.table.sql @@ -3,8 +3,10 @@ [ClientIdentifier] VARCHAR (255) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL, [ClientSecret] VARCHAR (255) COLLATE SQL_Latin1_General_CP1_CS_AS NULL, [Callback] VARCHAR (2048) NULL, - [Name] NVARCHAR (50) NULL + [Name] NVARCHAR (50) NOT NULL ); + + diff --git a/projecttemplates/RelyingPartyLogic/CreateDatabase.sql b/projecttemplates/RelyingPartyLogic/CreateDatabase.sql index 99df5e5..5c82398 100644 --- a/projecttemplates/RelyingPartyLogic/CreateDatabase.sql +++ b/projecttemplates/RelyingPartyLogic/CreateDatabase.sql @@ -195,7 +195,7 @@ CREATE TABLE [dbo].[Client] ( [ClientIdentifier] VARCHAR (255) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL, [ClientSecret] VARCHAR (255) COLLATE SQL_Latin1_General_CP1_CS_AS NULL, [Callback] VARCHAR (2048) NULL, - [Name] NVARCHAR (50) NULL + [Name] NVARCHAR (50) NOT NULL ); diff --git a/projecttemplates/RelyingPartyLogic/Model.Designer.cs b/projecttemplates/RelyingPartyLogic/Model.Designer.cs index 564bde5..8884760 100644 --- a/projecttemplates/RelyingPartyLogic/Model.Designer.cs +++ b/projecttemplates/RelyingPartyLogic/Model.Designer.cs @@ -15,7 +15,7 @@ [assembly: global::System.Data.Objects.DataClasses.EdmRelationshipAttribute("DatabaseModel", "FK_IssuedToken_User", "User", global::System.Data.Metadata.Edm.RelationshipMultiplicity.One, typeof(RelyingPartyLogic.User), "ClientAuthorization", global::System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(RelyingPartyLogic.ClientAuthorization))] // Original file name: -// Generation date: 7/14/2010 7:00:56 AM +// Generation date: 7/14/2010 9:35:17 PM namespace RelyingPartyLogic { @@ -1190,12 +1190,14 @@ namespace RelyingPartyLogic /// </summary> /// <param name="clientId">Initial value of ClientId.</param> /// <param name="clientIdentifier">Initial value of ClientIdentifier.</param> + /// <param name="name">Initial value of Name.</param> [global::System.CodeDom.Compiler.GeneratedCode("System.Data.Entity.Design.EntityClassGenerator", "4.0.0.0")] - public static Client CreateClient(int clientId, string clientIdentifier) + public static Client CreateClient(int clientId, string clientIdentifier, string name) { Client client = new Client(); client.ClientId = clientId; client.ClientIdentifier = clientIdentifier; + client.Name = name; return client; } /// <summary> @@ -1309,7 +1311,7 @@ namespace RelyingPartyLogic /// <summary> /// There are no comments for property Name in the schema. /// </summary> - [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute()] + [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)] [global::System.Runtime.Serialization.DataMemberAttribute()] [global::System.CodeDom.Compiler.GeneratedCode("System.Data.Entity.Design.EntityClassGenerator", "4.0.0.0")] public string Name @@ -1322,7 +1324,7 @@ namespace RelyingPartyLogic { this.OnNameChanging(value); this.ReportPropertyChanging("Name"); - this._Name = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value, true); + this._Name = global::System.Data.Objects.DataClasses.StructuralObject.SetValidValue(value, false); this.ReportPropertyChanged("Name"); this.OnNameChanged(); } diff --git a/projecttemplates/RelyingPartyLogic/Model.edmx b/projecttemplates/RelyingPartyLogic/Model.edmx index 2123935..a003493 100644 --- a/projecttemplates/RelyingPartyLogic/Model.edmx +++ b/projecttemplates/RelyingPartyLogic/Model.edmx @@ -55,7 +55,7 @@ <Property Name="ClientIdentifier" Type="varchar" Nullable="false" MaxLength="255" /> <Property Name="ClientSecret" Type="varchar" MaxLength="255" /> <Property Name="Callback" Type="varchar" MaxLength="2048" /> - <Property Name="Name" Type="nvarchar" MaxLength="50" /> + <Property Name="Name" Type="nvarchar" Nullable="false" MaxLength="50" /> </EntityType> <EntityType Name="ClientAuthorization"> <Key> @@ -284,7 +284,7 @@ <Property Type="String" Name="ClientIdentifier" Nullable="false" MaxLength="255" FixedLength="false" Unicode="true" /> <Property Type="String" Name="ClientSecret" MaxLength="255" FixedLength="false" Unicode="true" /> <Property Type="String" Name="CallbackAsString" MaxLength="2048" FixedLength="false" Unicode="true" /> - <Property Type="String" Name="Name" MaxLength="50" FixedLength="false" Unicode="true" /> + <Property Type="String" Name="Name" MaxLength="50" FixedLength="false" Unicode="true" Nullable="false" /> <NavigationProperty Name="ClientAuthorizations" Relationship="DatabaseModel.FK_IssuedToken_Consumer" FromRole="Client" ToRole="ClientAuthorization" /> </EntityType> <EntityType Name="ClientAuthorization"> diff --git a/projecttemplates/RelyingPartyLogic/OAuthAuthenticationModule.cs b/projecttemplates/RelyingPartyLogic/OAuthAuthenticationModule.cs index 3700b65..c0685bc 100644 --- a/projecttemplates/RelyingPartyLogic/OAuthAuthenticationModule.cs +++ b/projecttemplates/RelyingPartyLogic/OAuthAuthenticationModule.cs @@ -49,7 +49,7 @@ namespace RelyingPartyLogic { return; } - var tokenAnalyzer = new StandardAccessTokenAnalyzer(OAuthAuthorizationServer.AsymmetricKey, OAuthAuthorizationServer.AsymmetricKey); + var tokenAnalyzer = new SpecialAccessTokenAnalyzer(OAuthAuthorizationServer.AsymmetricKey, OAuthAuthorizationServer.AsymmetricKey); var resourceServer = new ResourceServer(tokenAnalyzer); IPrincipal principal; diff --git a/projecttemplates/RelyingPartyLogic/OAuthAuthorizationManager.cs b/projecttemplates/RelyingPartyLogic/OAuthAuthorizationManager.cs index f4e27a4..6ac2977 100644 --- a/projecttemplates/RelyingPartyLogic/OAuthAuthorizationManager.cs +++ b/projecttemplates/RelyingPartyLogic/OAuthAuthorizationManager.cs @@ -32,7 +32,7 @@ namespace RelyingPartyLogic { var httpDetails = operationContext.RequestContext.RequestMessage.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty; var requestUri = operationContext.RequestContext.RequestMessage.Properties["OriginalHttpRequestUri"] as Uri; - var tokenAnalyzer = new StandardAccessTokenAnalyzer(OAuthAuthorizationServer.AsymmetricKey, OAuthAuthorizationServer.AsymmetricKey); + var tokenAnalyzer = new SpecialAccessTokenAnalyzer(OAuthAuthorizationServer.AsymmetricKey, OAuthAuthorizationServer.AsymmetricKey); var resourceServer = new ResourceServer(tokenAnalyzer); try { diff --git a/projecttemplates/RelyingPartyLogic/OAuthAuthorizationServer.cs b/projecttemplates/RelyingPartyLogic/OAuthAuthorizationServer.cs index af3dba5..2b207f9 100644 --- a/projecttemplates/RelyingPartyLogic/OAuthAuthorizationServer.cs +++ b/projecttemplates/RelyingPartyLogic/OAuthAuthorizationServer.cs @@ -10,9 +10,12 @@ namespace RelyingPartyLogic { using System.Linq; using System.Security.Cryptography; using System.Text; + using System.Web; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.OAuth2; + using DotNetOpenAuth.OAuth2.ChannelElements; + using DotNetOpenAuth.OAuth2.Messages; /// <summary> /// Provides OAuth 2.0 authorization server information to DotNetOpenAuth. @@ -82,7 +85,11 @@ namespace RelyingPartyLogic { /// <returns>The client registration. Never null.</returns> /// <exception cref="ArgumentException">Thrown when no client with the given identifier is registered with this authorization server.</exception> public IConsumerDescription GetClient(string clientIdentifier) { - return Database.DataContext.Clients.First(c => c.ClientIdentifier == clientIdentifier); + try { + return Database.DataContext.Clients.First(c => c.ClientIdentifier == clientIdentifier); + } catch (InvalidOperationException ex) { + throw new ArgumentOutOfRangeException("No client by that identifier.", ex); + } } /// <summary> @@ -107,11 +114,63 @@ namespace RelyingPartyLogic { /// security in the event the user was revoking access in order to sever authorization on a stolen /// account or piece of hardware in which the tokens were stored. </para> /// </remarks> - public bool IsAuthorizationValid(DotNetOpenAuth.OAuth2.ChannelElements.IAuthorizationDescription authorization) { - // We don't support revoking tokens yet. - return true; + public bool IsAuthorizationValid(IAuthorizationDescription authorization) { + return this.IsAuthorizationValid(authorization.Scope, authorization.ClientIdentifier, authorization.UtcIssued, authorization.User); } #endregion + + public bool CanBeAutoApproved(EndUserAuthorizationRequest authorizationRequest) { + if (authorizationRequest == null) { + throw new ArgumentNullException("authorizationRequest"); + } + + // NEVER issue an auto-approval to a client that would end up getting an access token immediately + // (without a client secret), as that would allow ANY client to spoof an approved client's identity + // and obtain unauthorized access to user data. + if (authorizationRequest.ResponseType == EndUserAuthorizationResponseType.AuthorizationCode) { + // Never issue auto-approval if the client secret is blank, since that too makes it easy to spoof + // a client's identity and obtain unauthorized access. + var requestingClient = Database.DataContext.Clients.First(c => c.ClientIdentifier == authorizationRequest.ClientIdentifier); + if (!string.IsNullOrEmpty(requestingClient.ClientSecret)) { + return this.IsAuthorizationValid( + authorizationRequest.Scope, + authorizationRequest.ClientIdentifier, + DateTime.UtcNow, + HttpContext.Current.User.Identity.Name); + } + } + + // Default to not auto-approving. + return false; + } + + private bool IsAuthorizationValid(string requestedScope, string clientIdentifier, DateTime issuedUtc, string username) + { + var stringCompare = StringComparer.Ordinal; + var requestedScopes = OAuthUtilities.BreakUpScopes(requestedScope, stringCompare); + + var grantedScopeStrings = from auth in Database.DataContext.ClientAuthorizations + where + auth.Client.ClientIdentifier == clientIdentifier && + auth.CreatedOnUtc <= issuedUtc && + auth.User.AuthenticationTokens.Any(token => token.ClaimedIdentifier == username) + select auth.Scope; + + if (!grantedScopeStrings.Any()) { + // No granted authorizations prior to the issuance of this token, so it must have been revoked. + // Even if later authorizations restore this client's ability to call in, we can't allow + // access tokens issued before the re-authorization because the revoked authorization should + // effectively and permanently revoke all access and refresh tokens. + return false; + } + + var grantedScopes = new HashSet<string>(stringCompare); + foreach (string scope in grantedScopeStrings) { + grantedScopes.UnionWith(OAuthUtilities.BreakUpScopes(scope, stringCompare)); + } + + return requestedScopes.IsSubsetOf(grantedScopes); + } } } diff --git a/projecttemplates/RelyingPartyLogic/RelyingPartyLogic.csproj b/projecttemplates/RelyingPartyLogic/RelyingPartyLogic.csproj index 21215b0..06dee41 100644 --- a/projecttemplates/RelyingPartyLogic/RelyingPartyLogic.csproj +++ b/projecttemplates/RelyingPartyLogic/RelyingPartyLogic.csproj @@ -127,6 +127,7 @@ <Compile Include="Policies.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="RelyingPartyApplicationDbStore.cs" /> + <Compile Include="SpecialAccessTokenAnalyzer.cs" /> <Compile Include="Utilities.cs" /> </ItemGroup> <ItemGroup> diff --git a/projecttemplates/RelyingPartyLogic/SpecialAccessTokenAnalyzer.cs b/projecttemplates/RelyingPartyLogic/SpecialAccessTokenAnalyzer.cs new file mode 100644 index 0000000..f189433 --- /dev/null +++ b/projecttemplates/RelyingPartyLogic/SpecialAccessTokenAnalyzer.cs @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------- +// <copyright file="SpecialAccessTokenAnalyzer.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace RelyingPartyLogic { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + + using DotNetOpenAuth.OAuth2; + + internal class SpecialAccessTokenAnalyzer : StandardAccessTokenAnalyzer { + /// <summary> + /// Initializes a new instance of the <see cref="SpecialAccessTokenAnalyzer"/> class. + /// </summary> + /// <param name="authorizationServerPublicSigningKey">The authorization server public signing key.</param> + /// <param name="resourceServerPrivateEncryptionKey">The resource server private encryption key.</param> + internal SpecialAccessTokenAnalyzer(RSAParameters authorizationServerPublicSigningKey, RSAParameters resourceServerPrivateEncryptionKey) + : base(authorizationServerPublicSigningKey, resourceServerPrivateEncryptionKey) { + } + + public override bool TryValidateAccessToken(DotNetOpenAuth.Messaging.IDirectedProtocolMessage message, string accessToken, out string user, out string scope) { + bool result = base.TryValidateAccessToken(message, accessToken, out user, out scope); + if (result) { + // Ensure that clients coming in this way always belong to the oauth_client role. + scope += " " + "oauth_client"; + } + + return result; + } + } +} diff --git a/projecttemplates/WebFormsRelyingParty/Members/AccountInfo.aspx b/projecttemplates/WebFormsRelyingParty/Members/AccountInfo.aspx index 86263aa..458d624 100644 --- a/projecttemplates/WebFormsRelyingParty/Members/AccountInfo.aspx +++ b/projecttemplates/WebFormsRelyingParty/Members/AccountInfo.aspx @@ -91,7 +91,9 @@ <li> <asp:Label runat="server" Text='<%# HttpUtility.HtmlEncode(Eval("Client.Name").ToString()) %>' /> - - <asp:Label ID="Label1" runat="server" Text='<%# HttpUtility.HtmlEncode(Eval("CreatedOn").ToString()) %>' ForeColor="Gray" /> + <asp:Label ID="Label2" runat="server" Text='<%# HttpUtility.HtmlEncode((string)Eval("Scope")) %>' ForeColor="Gray" /> + - + <asp:Label ID="Label1" runat="server" Text='<%# HttpUtility.HtmlEncode(Eval("CreatedOnUtc").ToString()) %>' ForeColor="Gray" /> - <asp:LinkButton ID="revokeLink" runat="server" Text="revoke" OnCommand="revokeToken_Command" CommandName="revokeToken" CommandArgument='<%# Eval("AuthorizationId") %>' /> diff --git a/projecttemplates/WebFormsRelyingParty/Members/OAuthAuthorize.aspx b/projecttemplates/WebFormsRelyingParty/Members/OAuthAuthorize.aspx index 7e07323..9ec00a8 100644 --- a/projecttemplates/WebFormsRelyingParty/Members/OAuthAuthorize.aspx +++ b/projecttemplates/WebFormsRelyingParty/Members/OAuthAuthorize.aspx @@ -5,68 +5,45 @@ <h2> Client authorization </h2> - <asp:MultiView runat="server" ID="outerMultiView" ActiveViewIndex="0"> - <asp:View runat="server" ID="getPermissionView"> - <div style="background-color: Yellow"> - <b>Warning</b>: Never give your login credentials to another web site or application. - </div> - <p> - The - <asp:Label ID="consumerNameLabel" runat="server" Text="(app name)" /> - application is requesting to access the private data in your account here. Is that - alright with you? - </p> - <p> - If you grant access now, you can revoke it at any time by returning to <a href="AccountInfo.aspx" - target="_blank">your account page</a>. - </p> - <div style="display: none" id="responseButtonsDiv"> - <asp:Button ID="yesButton" runat="server" Text="Yes" OnClick="yesButton_Click" /> - <asp:Button ID="noButton" runat="server" Text="No" OnClick="noButton_Click" /> - <asp:HiddenField runat="server" ID="csrfCheck" EnableViewState="false" /> - </div> - <div id="javascriptDisabled"> - <b>Javascript appears to be disabled in your browser. </b>This page requires Javascript - to be enabled to better protect your security. - </div> + <div style="background-color: Yellow"> + <b>Warning</b>: Never give your login credentials to another web site or application. + </div> + <p> + The + <asp:Label ID="consumerNameLabel" runat="server" Text="(app name)" /> + application is requesting to access the private data in your account here. Is that + alright with you? + </p> + <p> + <b>Requested access: </b> + <asp:Label runat="server" ID="scopeLabel" /> + </p> + <p> + If you grant access now, you can revoke it at any time by returning to <a href="AccountInfo.aspx" + target="_blank">your account page</a>. + </p> + <div style="display: none" id="responseButtonsDiv"> + <asp:Button ID="yesButton" runat="server" Text="Yes" OnClick="yesButton_Click" /> + <asp:Button ID="noButton" runat="server" Text="No" OnClick="noButton_Click" /> + <asp:HiddenField runat="server" ID="csrfCheck" EnableViewState="false" /> + </div> + <div id="javascriptDisabled"> + <b>Javascript appears to be disabled in your browser. </b>This page requires Javascript + to be enabled to better protect your security. + </div> - <script language="javascript" type="text/javascript"> - //<![CDATA[ - // we use HTML to hide the action buttons and Javascript to show them - // to protect against click-jacking in an iframe whose javascript is disabled. - document.getElementById('responseButtonsDiv').style.display = 'block'; - document.getElementById('javascriptDisabled').style.display = 'none'; + <script language="javascript" type="text/javascript"> + //<![CDATA[ + // we use HTML to hide the action buttons and Javascript to show them + // to protect against click-jacking in an iframe whose javascript is disabled. + document.getElementById('responseButtonsDiv').style.display = 'block'; + document.getElementById('javascriptDisabled').style.display = 'none'; - // Frame busting code (to protect us from being hosted in an iframe). - // This protects us from click-jacking. - if (document.location !== window.top.location) { - window.top.location = document.location; - } - //]]> - </script> - - </asp:View> - <asp:View ID="authorizationGrantedView" runat="server"> - <p> - Authorization has been granted.</p> - <asp:MultiView runat="server" ID="verifierMultiView" ActiveViewIndex="0"> - <asp:View ID="verificationCodeView" runat="server"> - <p> - You must enter this verification code at the Consumer: - <asp:Label runat="server" ID="verificationCodeLabel" /> - </p> - </asp:View> - <asp:View ID="noCallbackView" runat="server"> - <p> - You may now close this window and return to the Consumer. - </p> - </asp:View> - </asp:MultiView> - </asp:View> - <asp:View ID="authorizationDeniedView" runat="server"> - <p> - Authorization has been denied. You're free to do whatever now. - </p> - </asp:View> - </asp:MultiView> + // Frame busting code (to protect us from being hosted in an iframe). + // This protects us from click-jacking. + if (document.location !== window.top.location) { + window.top.location = document.location; + } + //]]> + </script> </asp:Content> diff --git a/projecttemplates/WebFormsRelyingParty/Members/OAuthAuthorize.aspx.cs b/projecttemplates/WebFormsRelyingParty/Members/OAuthAuthorize.aspx.cs index c7355c3..2a95b89 100644 --- a/projecttemplates/WebFormsRelyingParty/Members/OAuthAuthorize.aspx.cs +++ b/projecttemplates/WebFormsRelyingParty/Members/OAuthAuthorize.aspx.cs @@ -7,17 +7,16 @@ namespace WebFormsRelyingParty.Members { using System; using System.Collections.Generic; + using System.Globalization; using System.Linq; using System.Net; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; - using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth; using DotNetOpenAuth.OAuth.Messages; using DotNetOpenAuth.OAuth2.Messages; - using RelyingPartyLogic; public partial class OAuthAuthorize : System.Web.UI.Page { @@ -27,8 +26,8 @@ namespace WebFormsRelyingParty.Members { // We'll mask that on postback it's a POST when looking up the authorization details so that the GET-only // message can be picked up. var requestInfo = this.IsPostBack - ? new HttpRequestInfo("GET", this.Request.Url, this.Request.RawUrl, new WebHeaderCollection(), null) - : null; + ? new HttpRequestInfo("GET", this.Request.Url, this.Request.RawUrl, new WebHeaderCollection(), null) + : null; this.pendingRequest = OAuthServiceProvider.AuthorizationServer.ReadAuthorizationRequest(requestInfo); if (this.pendingRequest == null) { Response.Redirect("AccountInfo.aspx"); @@ -38,27 +37,30 @@ namespace WebFormsRelyingParty.Members { this.csrfCheck.Value = Code.SiteUtilities.SetCsrfCookie(); var requestingClient = Database.DataContext.Clients.First(c => c.ClientIdentifier == this.pendingRequest.ClientIdentifier); this.consumerNameLabel.Text = HttpUtility.HtmlEncode(requestingClient.Name); + this.scopeLabel.Text = HttpUtility.HtmlEncode(this.pendingRequest.Scope); + + // Consider auto-approving if safe to do so. + if (((OAuthAuthorizationServer)OAuthServiceProvider.AuthorizationServer.AuthorizationServer).CanBeAutoApproved(this.pendingRequest)) { + OAuthServiceProvider.AuthorizationServer.ApproveAuthorizationRequest(this.pendingRequest, HttpContext.Current.User.Identity.Name); + } } else { Code.SiteUtilities.VerifyCsrfCookie(this.csrfCheck.Value); } } protected void yesButton_Click(object sender, EventArgs e) { - this.outerMultiView.SetActiveView(this.authorizationGrantedView); - var requestingClient = Database.DataContext.Clients.First(c => c.ClientIdentifier == this.pendingRequest.ClientIdentifier); Database.LoggedInUser.ClientAuthorizations.Add( - new ClientAuthorization - { - Client = requestingClient, - Scope = this.pendingRequest.Scope, - User = Database.LoggedInUser, - }); + new ClientAuthorization { + Client = requestingClient, + Scope = this.pendingRequest.Scope, + User = Database.LoggedInUser, + CreatedOnUtc = DateTime.UtcNow.CutToSecond(), + }); OAuthServiceProvider.AuthorizationServer.ApproveAuthorizationRequest(this.pendingRequest, HttpContext.Current.User.Identity.Name); } protected void noButton_Click(object sender, EventArgs e) { - this.outerMultiView.SetActiveView(this.authorizationDeniedView); OAuthServiceProvider.AuthorizationServer.RejectAuthorizationRequest(this.pendingRequest); } } diff --git a/projecttemplates/WebFormsRelyingParty/Members/OAuthAuthorize.aspx.designer.cs b/projecttemplates/WebFormsRelyingParty/Members/OAuthAuthorize.aspx.designer.cs index 19947de..d243c81 100644 --- a/projecttemplates/WebFormsRelyingParty/Members/OAuthAuthorize.aspx.designer.cs +++ b/projecttemplates/WebFormsRelyingParty/Members/OAuthAuthorize.aspx.designer.cs @@ -13,31 +13,22 @@ namespace WebFormsRelyingParty.Members { public partial class OAuthAuthorize { /// <summary> - /// outerMultiView 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.MultiView outerMultiView; - - /// <summary> - /// getPermissionView control. + /// consumerNameLabel 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.View getPermissionView; + protected global::System.Web.UI.WebControls.Label consumerNameLabel; /// <summary> - /// consumerNameLabel control. + /// scopeLabel 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 consumerNameLabel; + protected global::System.Web.UI.WebControls.Label scopeLabel; /// <summary> /// yesButton control. @@ -65,59 +56,5 @@ namespace WebFormsRelyingParty.Members { /// To modify move field declaration from designer file to code-behind file. /// </remarks> protected global::System.Web.UI.WebControls.HiddenField csrfCheck; - - /// <summary> - /// authorizationGrantedView 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.View authorizationGrantedView; - - /// <summary> - /// verifierMultiView 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.MultiView verifierMultiView; - - /// <summary> - /// verificationCodeView 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.View verificationCodeView; - - /// <summary> - /// verificationCodeLabel 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 verificationCodeLabel; - - /// <summary> - /// noCallbackView 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.View noCallbackView; - - /// <summary> - /// authorizationDeniedView 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.View authorizationDeniedView; } } diff --git a/projecttemplates/WebFormsRelyingParty/Members/Web.config b/projecttemplates/WebFormsRelyingParty/Members/Web.config index f95a16d..4ab44bc 100644 --- a/projecttemplates/WebFormsRelyingParty/Members/Web.config +++ b/projecttemplates/WebFormsRelyingParty/Members/Web.config @@ -20,7 +20,7 @@ <location path="AccountInfo.aspx"> <system.web> <authorization> - <deny roles="delegated" /> + <deny roles="oauth_client" /> </authorization> </system.web> </location> diff --git a/samples/OAuthConsumerWpf/Authorize2.xaml.cs b/samples/OAuthConsumerWpf/Authorize2.xaml.cs index e257315..8cf9f6f 100644 --- a/samples/OAuthConsumerWpf/Authorize2.xaml.cs +++ b/samples/OAuthConsumerWpf/Authorize2.xaml.cs @@ -22,13 +22,14 @@ public partial class Authorize2 : Window { private UserAgentClient client; - internal Authorize2(UserAgentClient client) { + internal Authorize2(UserAgentClient client, IAuthorizationState authorizationState) { Contract.Requires(client != null, "client"); + Contract.Requires(authorizationState != null, "authorizationState"); InitializeComponent(); this.client = client; - this.Authorization = new AuthorizationState(); + this.Authorization = authorizationState; Uri authorizationUrl = this.client.RequestUserAuthorization(this.Authorization); this.webBrowser.Navigate(authorizationUrl.AbsoluteUri); // use AbsoluteUri to workaround bug in WebBrowser that calls Uri.ToString instead of Uri.AbsoluteUri leading to escaping errors. } diff --git a/samples/OAuthConsumerWpf/MainWindow.xaml b/samples/OAuthConsumerWpf/MainWindow.xaml index 2305227..40b63e7 100644 --- a/samples/OAuthConsumerWpf/MainWindow.xaml +++ b/samples/OAuthConsumerWpf/MainWindow.xaml @@ -144,6 +144,7 @@ <RowDefinition Height="auto" /> <RowDefinition Height="auto" /> <RowDefinition Height="auto" /> + <RowDefinition Height="auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> @@ -152,10 +153,10 @@ <ColumnDefinition Width="auto" /> </Grid.ColumnDefinitions> <Label Grid.Row="1" TabIndex="202">Token Endpoint URL</Label> - <TextBox Grid.Row="1" Grid.Column="1" x:Name="wrapTokenUrlBox" Text="https://graph.facebook.com/oauth/access_token" TabIndex="203" /> + <TextBox Grid.Row="1" Grid.Column="1" x:Name="wrapTokenUrlBox" Text="http://localhost:54189/OAuthTokenEndpoint.ashx" TabIndex="203" /> <Label Grid.Row="1" Grid.Column="2" TabIndex="204">POST</Label> <Label Grid.Row="2" TabIndex="205">User Authorization URL</Label> - <TextBox Grid.Row="2" Grid.Column="1" x:Name="wrapAuthorizationUrlBox" Text="https://graph.facebook.com/oauth/authorize?display=popup" TabIndex="206" /> + <TextBox Grid.Row="2" Grid.Column="1" x:Name="wrapAuthorizationUrlBox" Text="http://localhost:54189/Members/OAuthAuthorize.aspx" TabIndex="206" /> <Label Grid.Row="2" Grid.Column="2" TabIndex="207">GET</Label> <Label Grid.Row="0" TabIndex="200">Grant Type</Label> <ComboBox Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" x:Name="flowBox" SelectedIndex="0" TabIndex="201"> @@ -166,7 +167,7 @@ </ComboBox.Items> </ComboBox> <Label Grid.Row="3" TabIndex="207">Resource URL</Label> - <TextBox Grid.Row="3" Grid.Column="1" x:Name="wrapResourceUrlBox" Text="https://graph.facebook.com/me" TabIndex="208" /> + <TextBox Grid.Row="3" Grid.Column="1" x:Name="wrapResourceUrlBox" Text="http://localhost:54189/Members/" TabIndex="208" /> <ComboBox Grid.Row="3" Grid.Column="2" x:Name="wrapResourceHttpMethodList" SelectedIndex="0" TabIndex="209"> <ComboBox.Items> <ComboBoxItem>GET w/ header</ComboBoxItem> @@ -175,17 +176,19 @@ </ComboBox.Items> </ComboBox> <Label Grid.Row="4" TabIndex="210">Client Identifier</Label> - <TextBox Grid.Row="4" Grid.Column="1" x:Name="wrapClientIdentifierBox" Grid.ColumnSpan="2" Text="367207604173" TabIndex="211" /> + <TextBox Grid.Row="4" Grid.Column="1" x:Name="wrapClientIdentifierBox" Grid.ColumnSpan="2" Text="a" TabIndex="211" /> <Label Grid.Row="5" TabIndex="212">Client Secret</Label> - <TextBox Grid.Row="5" Grid.Column="1" x:Name="wrapClientSecretBox" Grid.ColumnSpan="2" Text="1df77e64055c4d7d3583cefdf2bc62d7" TabIndex="213" /> - <Label Grid.Row="6" TabIndex="214">OAuth 2.0 version</Label> - <ComboBox Grid.Row="6" Grid.Column="1" SelectedIndex="0" x:Name="wrapVersion" TabIndex="215"> + <TextBox Grid.Row="5" Grid.Column="1" x:Name="wrapClientSecretBox" Grid.ColumnSpan="2" Text="b" TabIndex="213" /> + <Label Grid.Row="6" TabIndex="214">Scope</Label> + <TextBox Grid.Row="6" Grid.Column="1" x:Name="wrapScopeBox" TabIndex="215" Text="some scope" /> + <Label Grid.Row="7" TabIndex="216">OAuth 2.0 version</Label> + <ComboBox Grid.Row="7" Grid.Column="1" SelectedIndex="0" x:Name="wrapVersion" TabIndex="217"> <ComboBox.Items> <ComboBoxItem>2.0 DRAFT 9</ComboBoxItem> </ComboBox.Items> </ComboBox> - <Button Grid.Row="7" Grid.Column="1" x:Name="wrapBeginButton" Click="wrapBeginButton_Click" TabIndex="216">Begin</Button> - <TextBox Grid.Column="0" Grid.Row="8" Grid.ColumnSpan="3" Name="wrapResultsBox" IsReadOnly="True" /> + <Button Grid.Row="8" Grid.Column="1" x:Name="wrapBeginButton" Click="wrapBeginButton_Click" TabIndex="218">Begin</Button> + <TextBox Grid.Column="0" Grid.Row="9" Grid.ColumnSpan="3" Name="wrapResultsBox" IsReadOnly="True" TabIndex="219"/> </Grid> </TabItem> </TabControl> diff --git a/samples/OAuthConsumerWpf/MainWindow.xaml.cs b/samples/OAuthConsumerWpf/MainWindow.xaml.cs index d698ce0..46a5f06 100644 --- a/samples/OAuthConsumerWpf/MainWindow.xaml.cs +++ b/samples/OAuthConsumerWpf/MainWindow.xaml.cs @@ -29,7 +29,11 @@ using DotNetOpenAuth.OAuth; using DotNetOpenAuth.OAuth.ChannelElements; using DotNetOpenAuth.Samples.OAuthConsumerWpf.WcfSampleService; + + using OAuth2; + using OAuth2 = DotNetOpenAuth.OAuth2; + using ProtocolVersion = DotNetOpenAuth.OAuth.ProtocolVersion; /// <summary> /// Interaction logic for MainWindow.xaml @@ -213,8 +217,10 @@ ////var client = new DotNetOpenAuth.OAuth2.WebAppClient(authServer); ////client.PrepareRequestUserAuthorization(); var client = new OAuth2.UserAgentClient(authServer, wrapClientIdentifierBox.Text); + client.ClientSecret = wrapClientSecretBox.Text; - var authorizePopup = new Authorize2(client); + var authorization = new AuthorizationState { Scope = wrapScopeBox.Text }; + var authorizePopup = new Authorize2(client, authorization); authorizePopup.Owner = this; bool? result = authorizePopup.ShowDialog(); if (result.HasValue && result.Value) { @@ -237,6 +243,8 @@ } } catch (DotNetOpenAuth.Messaging.ProtocolException ex) { MessageBox.Show(this, ex.Message); + } catch (WebException ex) { + MessageBox.Show(this, ex.Message); } } } diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index 60fbdd8..0f44711 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -209,6 +209,15 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Cuts off precision beyond a second on a DateTime value. + /// </summary> + /// <param name="value">The value.</param> + /// <returns>A DateTime with a 0 millisecond component.</returns> + public static DateTime CutToSecond(this DateTime value) { + return value - TimeSpan.FromMilliseconds(value.Millisecond); + } + + /// <summary> /// Strips any and all URI query parameters that serve as parts of a message. /// </summary> /// <param name="uri">The URI that may contain query parameters to remove.</param> diff --git a/src/DotNetOpenAuth/OAuth2/ChannelElements/ITokenCarryingRequest.cs b/src/DotNetOpenAuth/OAuth2/ChannelElements/ITokenCarryingRequest.cs index 0f4d84f..4c8d33f 100644 --- a/src/DotNetOpenAuth/OAuth2/ChannelElements/ITokenCarryingRequest.cs +++ b/src/DotNetOpenAuth/OAuth2/ChannelElements/ITokenCarryingRequest.cs @@ -5,6 +5,8 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System.Security.Cryptography; + using Messaging; /// <summary> diff --git a/src/DotNetOpenAuth/OAuth2/IConsumerDescription.cs b/src/DotNetOpenAuth/OAuth2/IConsumerDescription.cs index bfb3ecc..4e2bc4f 100644 --- a/src/DotNetOpenAuth/OAuth2/IConsumerDescription.cs +++ b/src/DotNetOpenAuth/OAuth2/IConsumerDescription.cs @@ -6,14 +6,13 @@ namespace DotNetOpenAuth.OAuth2 { using System; - using System.Security.Cryptography.X509Certificates; /// <summary> /// A description of a client from an Authorization Server's point of view. /// </summary> public interface IConsumerDescription { /// <summary> - /// Gets the consumer secret. + /// Gets the client secret. /// </summary> string Secret { get; } diff --git a/src/DotNetOpenAuth/OAuth2/Messages/EndUserAuthorizationSuccessResponseBase.cs b/src/DotNetOpenAuth/OAuth2/Messages/EndUserAuthorizationSuccessResponseBase.cs index 62cad53..a02c050 100644 --- a/src/DotNetOpenAuth/OAuth2/Messages/EndUserAuthorizationSuccessResponseBase.cs +++ b/src/DotNetOpenAuth/OAuth2/Messages/EndUserAuthorizationSuccessResponseBase.cs @@ -7,6 +7,8 @@ namespace DotNetOpenAuth.OAuth2.Messages { using System; using System.Diagnostics.Contracts; + using System.Security.Cryptography; + using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth2.ChannelElements; diff --git a/src/DotNetOpenAuth/OAuth2/OAuthUtilities.cs b/src/DotNetOpenAuth/OAuth2/OAuthUtilities.cs index ccc7e8d..fa1e661 100644 --- a/src/DotNetOpenAuth/OAuth2/OAuthUtilities.cs +++ b/src/DotNetOpenAuth/OAuth2/OAuthUtilities.cs @@ -17,7 +17,52 @@ namespace DotNetOpenAuth.OAuth2 { /// <summary> /// Some common utility methods for OAuth 2.0. /// </summary> - internal static class OAuthUtilities { + public static class OAuthUtilities { + /// <summary> + /// The delimiter between scope elements. + /// </summary> + private static char[] scopeDelimiter = new char[] { ' ' }; + + /// <summary> + /// Determines whether one given scope is a subset of another scope. + /// </summary> + /// <param name="requestedScope">The requested scope, which may be a subset of <paramref name="grantedScope"/>.</param> + /// <param name="grantedScope">The granted scope, the suspected superset.</param> + /// <returns> + /// <c>true</c> if all the elements that appear in <paramref name="requestedScope"/> also appear in <paramref name="grantedScope"/>; + /// <c>false</c> otherwise. + /// </returns> + public static bool IsScopeSubset(string requestedScope, string grantedScope) { + if (string.IsNullOrEmpty(requestedScope)) { + return true; + } + + if (string.IsNullOrEmpty(grantedScope)) { + return false; + } + + var requestedScopes = new HashSet<string>(requestedScope.Split(scopeDelimiter, StringSplitOptions.RemoveEmptyEntries)); + var grantedScopes = new HashSet<string>(grantedScope.Split(scopeDelimiter, StringSplitOptions.RemoveEmptyEntries)); + return requestedScopes.IsSubsetOf(grantedScopes); + } + + /// <summary> + /// Identifies individual scope elements + /// </summary> + /// <param name="scope">The scope.</param> + /// <param name="scopeComparer">The scope comparer, allowing scopes to be case sensitive or insensitive. + /// Usually <see cref="StringComparer.Ordinal"/> or <see cref="StringComparer.OrdinalIgnoreCase"/>.</param> + /// <returns></returns> + public static HashSet<string> BreakUpScopes(string scope, StringComparer scopeComparer) { + Contract.Requires<ArgumentNullException>(scopeComparer != null, "scopeComparer"); + + if (string.IsNullOrEmpty(scope)) { + return new HashSet<string>(); + } + + return new HashSet<string>(scope.Split(scopeDelimiter, StringSplitOptions.RemoveEmptyEntries), scopeComparer); + } + /// <summary> /// Authorizes an HTTP request using an OAuth 2.0 access token in an HTTP Authorization header. /// </summary> diff --git a/src/DotNetOpenAuth/OAuth2/StandardAccessTokenAnalyzer.cs b/src/DotNetOpenAuth/OAuth2/StandardAccessTokenAnalyzer.cs index f292af5..5e0ea94 100644 --- a/src/DotNetOpenAuth/OAuth2/StandardAccessTokenAnalyzer.cs +++ b/src/DotNetOpenAuth/OAuth2/StandardAccessTokenAnalyzer.cs @@ -50,7 +50,7 @@ namespace DotNetOpenAuth.OAuth2 { /// This method also responsible to throw a <see cref="ProtocolException"/> or return /// <c>false</c> when the access token is expired, invalid, or from an untrusted authorization server. /// </remarks> - public bool TryValidateAccessToken(IDirectedProtocolMessage message, string accessToken, out string user, out string scope) { + public virtual bool TryValidateAccessToken(IDirectedProtocolMessage message, string accessToken, out string user, out string scope) { var accessTokenFormatter = AccessToken.CreateFormatter(this.AuthorizationServerPublicSigningKey, this.ResourceServerPrivateEncryptionKey); var token = accessTokenFormatter.Deserialize(message, accessToken); user = token.User; diff --git a/src/DotNetOpenAuth/OAuth2/UserAgentClient.cs b/src/DotNetOpenAuth/OAuth2/UserAgentClient.cs index f7e1a9f..db73cd9 100644 --- a/src/DotNetOpenAuth/OAuth2/UserAgentClient.cs +++ b/src/DotNetOpenAuth/OAuth2/UserAgentClient.cs @@ -36,6 +36,12 @@ namespace DotNetOpenAuth.OAuth2 { Contract.Requires<ArgumentNullException>(authorizationEndpoint != null, "authorizationEndpoint"); } + // TODO: remove this. user agent clients can't keep secrets. + public new string ClientSecret { + get { return base.ClientSecret; } + set { base.ClientSecret = value; } + } + /// <summary> /// Generates a URL that the user's browser can be directed to in order to authorize /// this client to access protected data at some resource server. @@ -65,7 +71,8 @@ namespace DotNetOpenAuth.OAuth2 { ClientIdentifier = this.ClientIdentifier, Scope = authorization.Scope, Callback = authorization.Callback, - ResponseType = EndUserAuthorizationResponseType.AccessToken, + // TODO: bring back ResponseType = AccessToken, since user agents can't keep secrets, thus can't process authorization codes. + //ResponseType = EndUserAuthorizationResponseType.AccessToken, }; return this.Channel.PrepareResponse(request).GetDirectUriRequest(this.Channel); diff --git a/src/DotNetOpenAuth/OAuth2/WebServerAuthorizationServer.cs b/src/DotNetOpenAuth/OAuth2/WebServerAuthorizationServer.cs index 8800efd..681f062 100644 --- a/src/DotNetOpenAuth/OAuth2/WebServerAuthorizationServer.cs +++ b/src/DotNetOpenAuth/OAuth2/WebServerAuthorizationServer.cs @@ -45,10 +45,16 @@ namespace DotNetOpenAuth.OAuth2 { return message; } - public void ApproveAuthorizationRequest(EndUserAuthorizationRequest authorizationRequest, string username, Uri callback = null) { + public void ApproveAuthorizationRequest(EndUserAuthorizationRequest authorizationRequest, string username, string scope = null, Uri callback = null) { Contract.Requires<ArgumentNullException>(authorizationRequest != null, "authorizationRequest"); var response = this.PrepareApproveAuthorizationRequest(authorizationRequest, username, callback); + + // Customize the approved scope if the authorization server has decided to do so. + if (scope != null) { + response.Scope = scope; + } + this.Channel.Send(response); } |