Skip to content
This repository was archived by the owner on Mar 20, 2019. It is now read-only.

Commit 0c8a4a3

Browse files
committed
AccessToken is now a public class.
Resource Servers can now handle access tokens that are issued for a client's data (not a 3rd party resource owner's). Client Identifiers are no longer included in access tokens for unauthenticated clients. More work needed on IAccessTokenAnalyzer and the access token formatter. We need to generalize the serialization itself so folks can use JWT, etc. We also still need access token to have a host-defined map of claims. Fixes #104 Fixes #102
1 parent 4fcf484 commit 0c8a4a3

11 files changed

Lines changed: 102 additions & 67 deletions

File tree

projecttemplates/RelyingPartyLogic/SpecialAccessTokenAnalyzer.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,13 @@ internal SpecialAccessTokenAnalyzer(RSACryptoServiceProvider authorizationServer
2323
: base(authorizationServerPublicSigningKey, resourceServerPrivateEncryptionKey) {
2424
}
2525

26-
public override bool TryValidateAccessToken(DotNetOpenAuth.Messaging.IDirectedProtocolMessage message, string accessToken, out string user, out HashSet<string> scope) {
27-
bool result = base.TryValidateAccessToken(message, accessToken, out user, out scope);
28-
if (result) {
29-
// Ensure that clients coming in this way always belong to the oauth_client role.
30-
scope.Add("oauth_client");
31-
}
26+
public override AccessToken DeserializeAccessToken(DotNetOpenAuth.Messaging.IDirectedProtocolMessage message, string accessToken) {
27+
var token = base.DeserializeAccessToken(message, accessToken);
3228

33-
return result;
29+
// Ensure that clients coming in this way always belong to the oauth_client role.
30+
token.Scope.Add("oauth_client");
31+
32+
return token;
3433
}
3534
}
3635
}

src/DotNetOpenAuth.Core/Messaging/DataBag.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace DotNetOpenAuth.Messaging {
1414
/// A collection of message parts that will be serialized into a single string,
1515
/// to be set into a larger message.
1616
/// </summary>
17-
internal abstract class DataBag : IMessage {
17+
public abstract class DataBag : IMessage {
1818
/// <summary>
1919
/// The default version for DataBags.
2020
/// </summary>

src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ public EndUserAuthorizationSuccessResponseBase PrepareApproveAuthorizationReques
184184
implicitGrantResponse.Lifetime = accessRequestInternal.AccessTokenCreationParameters.AccessTokenLifetime;
185185
IAccessTokenCarryingRequest tokenCarryingResponse = implicitGrantResponse;
186186
tokenCarryingResponse.AuthorizationDescription = new AccessToken(
187-
authorizationRequest.ClientIdentifier,
188187
implicitGrantResponse.Scope,
189188
userName,
190189
implicitGrantResponse.Lifetime);

src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/IAccessTokenAnalyzer.cs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,12 @@ public interface IAccessTokenAnalyzer {
2323
/// Reads an access token to find out what data it authorizes access to.
2424
/// </summary>
2525
/// <param name="message">The message carrying the access token.</param>
26-
/// <param name="accessToken">The access token.</param>
27-
/// <param name="user">The user whose data is accessible with this access token.</param>
28-
/// <param name="scope">The scope of access authorized by this access token.</param>
29-
/// <returns>A value indicating whether this access token is valid.</returns>
26+
/// <param name="accessToken">The access token's serialized representation.</param>
27+
/// <returns>The deserialized, validated token.</returns>
28+
/// <exception cref="ProtocolException">Thrown if the access token is expired, invalid, or from an untrusted authorization server.</exception>
3029
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "Try pattern")]
3130
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "Try pattern")]
32-
bool TryValidateAccessToken(IDirectedProtocolMessage message, string accessToken, out string user, out HashSet<string> scope);
31+
AccessToken DeserializeAccessToken(IDirectedProtocolMessage message, string accessToken);
3332
}
3433

3534
/// <summary>
@@ -47,17 +46,13 @@ private IAccessTokenAnalyzerContract() {
4746
/// Reads an access token to find out what data it authorizes access to.
4847
/// </summary>
4948
/// <param name="message">The message carrying the access token.</param>
50-
/// <param name="accessToken">The access token.</param>
51-
/// <param name="user">The user whose data is accessible with this access token.</param>
52-
/// <param name="scope">The scope of access authorized by this access token.</param>
53-
/// <returns>
54-
/// A value indicating whether this access token is valid.
55-
/// </returns>
56-
bool IAccessTokenAnalyzer.TryValidateAccessToken(IDirectedProtocolMessage message, string accessToken, out string user, out HashSet<string> scope) {
49+
/// <param name="accessToken">The access token's serialized representation.</param>
50+
/// <returns>The deserialized, validated token.</returns>
51+
/// <exception cref="ProtocolException">Thrown if the access token is expired, invalid, or from an untrusted authorization server.</exception>
52+
AccessToken IAccessTokenAnalyzer.DeserializeAccessToken(IDirectedProtocolMessage message, string accessToken) {
5753
Requires.NotNull(message, "message");
5854
Requires.NotNullOrEmpty(accessToken, "accessToken");
59-
Contract.Ensures(Contract.Result<bool>() == (Contract.ValueAtReturn<string>(out user) != null));
60-
55+
Contract.Ensures(Contract.Result<AccessToken>() != null);
6156
throw new NotImplementedException();
6257
}
6358
}

src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/OAuth2Strings.Designer.cs

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/OAuth2Strings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,16 @@
117117
<resheader name="writer">
118118
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
119119
</resheader>
120+
<data name="ClientIdentifierLooksLikeResourceOwnerName" xml:space="preserve">
121+
<value>Client Identifier starts with a resource owner prefix. Authorization aborted.</value>
122+
</data>
120123
<data name="InvalidAccessToken" xml:space="preserve">
121124
<value>Invalid access token.</value>
122125
</data>
123126
<data name="MissingAccessToken" xml:space="preserve">
124127
<value>Missing access token.</value>
125128
</data>
129+
<data name="ResourceOwnerNameLooksLikeClientIdentifier" xml:space="preserve">
130+
<value>Resource owner username starts with a client prefix. Authorization aborted.</value>
131+
</data>
126132
</root>

src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServer.cs

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public ResourceServer(IAccessTokenAnalyzer accessTokenAnalyzer) {
3434

3535
this.AccessTokenAnalyzer = accessTokenAnalyzer;
3636
this.Channel = new OAuth2ResourceServerChannel();
37+
this.ResourceOwnerPrincipalPrefix = string.Empty;
38+
this.ClientPrincipalPrefix = "client:";
3739
}
3840

3941
/// <summary>
@@ -42,6 +44,18 @@ public ResourceServer(IAccessTokenAnalyzer accessTokenAnalyzer) {
4244
/// <value>The access token analyzer.</value>
4345
public IAccessTokenAnalyzer AccessTokenAnalyzer { get; private set; }
4446

47+
/// <summary>
48+
/// Gets or sets the prefix to apply to a resource owner's username when used as the username in an <see cref="IPrincipal"/>.
49+
/// </summary>
50+
/// <value>The default value is the empty string.</value>
51+
public string ResourceOwnerPrincipalPrefix { get; set; }
52+
53+
/// <summary>
54+
/// Gets or sets the prefix to apply to a client identifier when used as the username in an <see cref="IPrincipal"/>.
55+
/// </summary>
56+
/// <value>The default value is "client:"</value>
57+
public string ClientPrincipalPrefix { get; set; }
58+
4559
/// <summary>
4660
/// Gets the channel.
4761
/// </summary>
@@ -51,50 +65,48 @@ public ResourceServer(IAccessTokenAnalyzer accessTokenAnalyzer) {
5165
/// <summary>
5266
/// Discovers what access the client should have considering the access token in the current request.
5367
/// </summary>
54-
/// <param name="userName">The name on the account the client has access to.</param>
55-
/// <param name="scope">The set of operations the client is authorized for.</param>
68+
/// <param name="accessToken">Receives the access token describing the authorization the client has.</param>
5669
/// <returns>An error to return to the client if access is not authorized; <c>null</c> if access is granted.</returns>
5770
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "0#", Justification = "Try pattern")]
5871
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "Try pattern")]
59-
public OutgoingWebResponse VerifyAccess(out string userName, out HashSet<string> scope) {
60-
return this.VerifyAccess(this.Channel.GetRequestFromContext(), out userName, out scope);
72+
public OutgoingWebResponse VerifyAccess(out AccessToken accessToken) {
73+
return this.VerifyAccess(this.Channel.GetRequestFromContext(), out accessToken);
6174
}
6275

6376
/// <summary>
6477
/// Discovers what access the client should have considering the access token in the current request.
6578
/// </summary>
6679
/// <param name="httpRequestInfo">The HTTP request info.</param>
67-
/// <param name="userName">The name on the account the client has access to.</param>
68-
/// <param name="scope">The set of operations the client is authorized for.</param>
80+
/// <param name="accessToken">Receives the access token describing the authorization the client has.</param>
6981
/// <returns>
7082
/// An error to return to the client if access is not authorized; <c>null</c> if access is granted.
7183
/// </returns>
7284
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "Try pattern")]
7385
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "Try pattern")]
74-
public virtual OutgoingWebResponse VerifyAccess(HttpRequestBase httpRequestInfo, out string userName, out HashSet<string> scope) {
86+
public virtual OutgoingWebResponse VerifyAccess(HttpRequestBase httpRequestInfo, out AccessToken accessToken) {
7587
Requires.NotNull(httpRequestInfo, "httpRequestInfo");
7688

7789
AccessProtectedResourceRequest request = null;
7890
try {
7991
if (this.Channel.TryReadFromRequest<AccessProtectedResourceRequest>(httpRequestInfo, out request)) {
80-
if (this.AccessTokenAnalyzer.TryValidateAccessToken(request, request.AccessToken, out userName, out scope)) {
81-
// No errors to return.
82-
return null;
92+
accessToken = this.AccessTokenAnalyzer.DeserializeAccessToken(request, request.AccessToken);
93+
ErrorUtilities.VerifyHost(accessToken != null, "IAccessTokenAnalyzer.DeserializeAccessToken returned a null reslut.");
94+
if (string.IsNullOrEmpty(accessToken.User) && string.IsNullOrEmpty(accessToken.ClientIdentifier)) {
95+
Logger.OAuth.Error("Access token rejected because both the username and client id properties were null or empty.");
96+
ErrorUtilities.ThrowProtocol(OAuth2Strings.InvalidAccessToken);
8397
}
8498

85-
throw ErrorUtilities.ThrowProtocol(OAuth2Strings.InvalidAccessToken);
99+
return null;
86100
} else {
87101
var response = new UnauthorizedResponse(new ProtocolException(OAuth2Strings.MissingAccessToken));
88102

89-
userName = null;
90-
scope = null;
103+
accessToken = null;
91104
return this.Channel.PrepareResponse(response);
92105
}
93106
} catch (ProtocolException ex) {
94107
var response = request != null ? new UnauthorizedResponse(request, ex) : new UnauthorizedResponse(ex);
95108

96-
userName = null;
97-
scope = null;
109+
accessToken = null;
98110
return this.Channel.PrepareResponse(response);
99111
}
100112
}
@@ -109,10 +121,23 @@ public virtual OutgoingWebResponse VerifyAccess(HttpRequestBase httpRequestInfo,
109121
/// </returns>
110122
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "Try pattern")]
111123
public virtual OutgoingWebResponse VerifyAccess(HttpRequestBase httpRequestInfo, out IPrincipal principal) {
112-
string username;
113-
HashSet<string> scope;
114-
var result = this.VerifyAccess(httpRequestInfo, out username, out scope);
115-
principal = result == null ? new OAuthPrincipal(username, scope != null ? scope.ToArray() : new string[0]) : null;
124+
AccessToken accessToken;
125+
var result = this.VerifyAccess(httpRequestInfo, out accessToken);
126+
if (result == null) {
127+
// Mitigates attacks on this approach of differentiating clients from resource owners
128+
// by checking that a username doesn't look suspiciously engineered to appear like the other type.
129+
ErrorUtilities.VerifyProtocol(accessToken.User == null || string.IsNullOrEmpty(this.ClientPrincipalPrefix) || !accessToken.User.StartsWith(this.ClientPrincipalPrefix, StringComparison.OrdinalIgnoreCase), OAuth2Strings.ResourceOwnerNameLooksLikeClientIdentifier);
130+
ErrorUtilities.VerifyProtocol(accessToken.ClientIdentifier == null || string.IsNullOrEmpty(this.ResourceOwnerPrincipalPrefix) || !accessToken.ClientIdentifier.StartsWith(this.ResourceOwnerPrincipalPrefix, StringComparison.OrdinalIgnoreCase), OAuth2Strings.ClientIdentifierLooksLikeResourceOwnerName);
131+
132+
string principalUserName = !string.IsNullOrEmpty(accessToken.User)
133+
? this.ResourceOwnerPrincipalPrefix + accessToken.User
134+
: this.ClientPrincipalPrefix + accessToken.ClientIdentifier;
135+
string[] principalScope = accessToken.Scope != null ? accessToken.Scope.ToArray() : new string[0];
136+
principal = new OAuthPrincipal(principalUserName, principalScope);
137+
} else {
138+
principal = null;
139+
}
140+
116141
return result;
117142
}
118143

src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/StandardAccessTokenAnalyzer.cs

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,13 @@ public StandardAccessTokenAnalyzer(RSACryptoServiceProvider authorizationServerP
4545
/// Reads an access token to find out what data it authorizes access to.
4646
/// </summary>
4747
/// <param name="message">The message carrying the access token.</param>
48-
/// <param name="accessToken">The access token.</param>
49-
/// <param name="user">The user whose data is accessible with this access token.</param>
50-
/// <param name="scope">The scope of access authorized by this access token.</param>
51-
/// <returns>
52-
/// A value indicating whether this access token is valid.
53-
/// </returns>
54-
/// <remarks>
55-
/// This method also responsible to throw a <see cref="ProtocolException"/> or return
56-
/// <c>false</c> when the access token is expired, invalid, or from an untrusted authorization server.
57-
/// </remarks>
58-
public virtual bool TryValidateAccessToken(IDirectedProtocolMessage message, string accessToken, out string user, out HashSet<string> scope) {
48+
/// <param name="accessToken">The access token's serialized representation.</param>
49+
/// <returns>The deserialized, validated token.</returns>
50+
/// <exception cref="ProtocolException">Thrown if the access token is expired, invalid, or from an untrusted authorization server.</exception>
51+
public virtual AccessToken DeserializeAccessToken(IDirectedProtocolMessage message, string accessToken) {
5952
var accessTokenFormatter = AccessToken.CreateFormatter(this.AuthorizationServerPublicSigningKey, this.ResourceServerPrivateEncryptionKey);
6053
var token = accessTokenFormatter.Deserialize(message, accessToken, Protocol.access_token);
61-
user = token.User;
62-
scope = new HashSet<string>(token.Scope, OAuthUtilities.ScopeStringComparer);
63-
return true;
54+
return token;
6455
}
6556
}
6657
}

src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
</PropertyGroup>
2020
<ItemGroup>
2121
<Compile Include="GlobalSuppressions.cs" />
22-
<Compile Include="OAuth2\ChannelElements\AccessToken.cs" />
22+
<Compile Include="OAuth2\AccessToken.cs" />
2323
<Compile Include="OAuth2\ChannelElements\AuthorizationDataBag.cs" />
2424
<Compile Include="OAuth2\ChannelElements\IAccessTokenCarryingRequest.cs" />
2525
<Compile Include="OAuth2\ChannelElements\ScopeEncoder.cs" />

src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AccessToken.cs renamed to src/DotNetOpenAuth.OAuth2/OAuth2/AccessToken.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@
44
// </copyright>
55
//-----------------------------------------------------------------------
66

7-
namespace DotNetOpenAuth.OAuth2.ChannelElements {
7+
namespace DotNetOpenAuth.OAuth2 {
88
using System;
99
using System.Collections.Generic;
1010
using System.Diagnostics.Contracts;
1111
using System.Security.Cryptography;
1212
using DotNetOpenAuth.Messaging;
1313
using DotNetOpenAuth.Messaging.Bindings;
14+
using DotNetOpenAuth.OAuth2.ChannelElements;
1415

1516
/// <summary>
1617
/// A short-lived token that accompanies HTTP requests to protected data to authorize the request.
1718
/// </summary>
18-
internal class AccessToken : AuthorizationDataBag {
19+
public class AccessToken : AuthorizationDataBag {
1920
/// <summary>
2021
/// Initializes a new instance of the <see cref="AccessToken"/> class.
2122
/// </summary>
@@ -40,14 +41,15 @@ internal AccessToken(IAuthorizationDescription authorization, TimeSpan? lifetime
4041
/// <summary>
4142
/// Initializes a new instance of the <see cref="AccessToken"/> class.
4243
/// </summary>
43-
/// <param name="clientIdentifier">The client identifier.</param>
4444
/// <param name="scopes">The scopes.</param>
4545
/// <param name="username">The username of the account that authorized this token.</param>
4646
/// <param name="lifetime">The lifetime for this access token.</param>
47-
internal AccessToken(string clientIdentifier, IEnumerable<string> scopes, string username, TimeSpan? lifetime) {
48-
Requires.NotNullOrEmpty(clientIdentifier, "clientIdentifier");
49-
50-
this.ClientIdentifier = clientIdentifier;
47+
/// <remarks>
48+
/// The <see cref="ClientIdentifier.ClientIdentifier"/> is left <c>null</c> in this case because this constructor
49+
/// is invoked in the case where the client is <em>not</em> authenticated, and therefore no
50+
/// trust in the client_id is appropriate.
51+
/// </remarks>
52+
internal AccessToken(IEnumerable<string> scopes, string username, TimeSpan? lifetime) {
5153
this.Scope.ResetContents(scopes);
5254
this.User = username;
5355
this.Lifetime = lifetime;
@@ -59,7 +61,7 @@ internal AccessToken(string clientIdentifier, IEnumerable<string> scopes, string
5961
/// </summary>
6062
/// <value>The lifetime.</value>
6163
[MessagePart(Encoder = typeof(TimespanSecondsEncoder))]
62-
internal TimeSpan? Lifetime { get; set; }
64+
public TimeSpan? Lifetime { get; set; }
6365

6466
/// <summary>
6567
/// Creates a formatter capable of serializing/deserializing an access token.

0 commit comments

Comments
 (0)