There seems to be 2 different questions here: about access token and about big list of roles.
Access Token
OAuth2 was designed to be able to handle high load and this requires some trade-offs. Particularly this is the reason why OAuth2 explicitly separates "Resource Server" and "Authorization Server" roles on the one hand, and "access token" and "refresh token" on the other hand. If for each request you have to check user authorization, it means that your Authorization Server should be able to handle all requests in your system. For high load systems this is not feasible.
OAuth2 allows you to make the following trade-off between performance and security: Authorization Server generates an Access Token that can be verified by the Resource Server without accessing Authorization Server (either at all or at least not more than once for a life of Authorization Server). This is effectively caching of the authorization information. So in this way you can drastically reduce load on your Authorization Server. The drawback is again the same as always with caching: authorization information might get stall. By varying access token life time you can tune performance vs. security balance.
This approach also might help if you do micro-services architecture where each service has own storage and don't access each other's.
Still if you don't have much load and you only have single Resource Server rather than tons of different services implemented using different technologies, there is nothing that prohibits you from actually doing full-blown validation on every request. I.e. yes, you may store Access Token in the DB, verify it on every access to the Resource Server and remove all Access Tokens when user is deleted, etc. But as @Evk noticed, if this is your scenario - OAuth2 is an overshoot for you.
Big list of roles
AFAIU OAuth2 doesn't provide an explicit feature for user roles. There is "Scopes" feature that might be also used for roles and its typical implementation it will produce too long string for 250 roles. Still OAuth2 doesn't explicitly specify any particular format for access token, so you can create a custom token that will hold roles information as a bit mask. Using base-64 encoding you can get 6 roles into a single character (64 = 2^6). So 250-300 roles will be manageable 40-50 chars.
JWT
Since you'll probably need some custom token anyway, you might be interested in the JSON Web Tokens aka JWT. In short JWT lets you specify custom additional payload (Private claims) and put your roles bitmask there.
You may actually use JWT alone without whole OAuth2 stuff if you don't really need any of the OAuth2 advanced features (such as scopes). Although JWT-tokens are supposed to be validated just by theis contents only, you may still store them in your local DB and do additional validation against the DB (as you were going to do with access refresh token).
Update Dec 1, 2017
If you want to use OWIN OAuth infrastructure, you can customize token format providing custom formatter via AccessTokenFormat
in OAuthBearerAuthenticationOptions
and OAuthAuthorizationServerOptions
. You may also override RefreshTokenFormat
.
Here is a sketch that shows how you can "compress" roles claims into a single bitmask:
- Define your
CustomRoles
enumeration that list all roles you have
[Flags]
public enum CustomRoles
{
Role1,
Role2,
Role3,
MaxRole // fake, for convenience
}
- Create
EncodeRoles
and DecodeRoles
methods to convert between IEnumerable<string>
format for roles and base64-encoded bit mask based on CustomRoles
defined above such as:
public static string EncodeRoles(IEnumerable<string> roles)
{
byte[] bitMask = new byte[(int)CustomRoles.MaxRole];
foreach (var role in roles)
{
CustomRoles roleIndex = (CustomRoles)Enum.Parse(typeof(CustomRoles), role);
var byteIndex = ((int)roleIndex) / 8;
var bitIndex = ((int)roleIndex) % 8;
bitMask[byteIndex] |= (byte)(1 << bitIndex);
}
return Convert.ToBase64String(bitMask);
}
public static IEnumerable<string> DecodeRoles(string encoded)
{
byte[] bitMask = Convert.FromBase64String(encoded);
var values = Enum.GetValues(typeof(CustomRoles)).Cast<CustomRoles>().Where(r => r != CustomRoles.MaxRole);
var roles = new List<string>();
foreach (var roleIndex in values)
{
var byteIndex = ((int)roleIndex) / 8;
var bitIndex = ((int)roleIndex) % 8;
if ((byteIndex < bitMask.Length) && (0 != (bitMask[byteIndex] & (1 << bitIndex))))
{
roles.Add(Enum.GetName(typeof(CustomRoles), roleIndex));
}
}
return roles;
}
- Use those methods in a custom implementation of
SecureDataFormat<AuthenticationTicket>
. For simplicity in this sketch I delegate most of work to standard OWIN components and just implement my CustomTicketSerializer
that creates another AuthenticationTicket
and uses standard DataSerializers.Ticket
. This is obviously not the most efficient way but it shows what you could do:
public class CustomTicketSerializer : IDataSerializer<AuthenticationTicket>
{
public const string RoleBitMaskType = "RoleBitMask";
private readonly IDataSerializer<AuthenticationTicket> _standardSerializers = DataSerializers.Ticket;
public static SecureDataFormat<AuthenticationTicket> CreateCustomTicketFormat(IAppBuilder app)
{
var tokenProtector = app.CreateDataProtector(typeof(OAuthAuthorizationServerMiddleware).Namespace, "Access_Token", "v1");
var customTokenFormat = new SecureDataFormat<AuthenticationTicket>(new CustomTicketSerializer(), tokenProtector, TextEncodings.Base64Url);
return customTokenFormat;
}
public byte[] Serialize(AuthenticationTicket ticket)
{
var identity = ticket.Identity;
var otherClaims = identity.Claims.Where(c => c.Type != identity.RoleClaimType);
var roleClaims = identity.Claims.Where(c => c.Type == identity.RoleClaimType);
var encodedRoleClaim = new Claim(RoleBitMaskType, EncodeRoles(roleClaims.Select(rc => rc.Value)));
var modifiedClaims = otherClaims.Concat(new Claim[] { encodedRoleClaim });
ClaimsIdentity modifiedIdentity = new ClaimsIdentity(modifiedClaims, identity.AuthenticationType, identity.NameClaimType, identity.RoleClaimType);
var modifiedTicket = new AuthenticationTicket(modifiedIdentity, ticket.Properties);
return _standardSerializers.Serialize(modifiedTicket);
}
public AuthenticationTicket Deserialize(byte[] data)
{
var ticket = _standardSerializers.Deserialize(data);
var identity = ticket.Identity;
var otherClaims = identity.Claims.Where(c => c.Type != RoleBitMaskType);
var encodedRoleClaim = identity.Claims.SingleOrDefault(c => c.Type == RoleBitMaskType);
if (encodedRoleClaim == null)
return ticket;
var roleClaims = DecodeRoles(encodedRoleClaim.Value).Select(r => new Claim(identity.RoleClaimType, r));
var modifiedClaims = otherClaims.Concat(roleClaims);
var modifiedIdentity = new ClaimsIdentity(modifiedClaims, identity.AuthenticationType, identity.NameClaimType, identity.RoleClaimType);
return new AuthenticationTicket(modifiedIdentity, ticket.Properties);
}
}
- In your
Startup.cs
configure OWIN to use your custom format such as:
var customTicketFormat = CustomTicketSerializer.CreateCustomTicketFormat(app);
OAuthBearerOptions.AccessTokenFormat = customTicketFormat;
OAuthServerOptions.AccessTokenFormat = customTicketFormat;
In your OAuthAuthorizationServerProvider
add ClaimTypes.Role
to the ClaimsIdentity
for each role assigned to the user.
In your controller use standard AuthorizeAttribute
such as
[Authorize(Roles = "Role1")]
[Route("")]
public IHttpActionResult Get()
For convenience and some safety you may subclass AuthorizeAttribute
class to accept CustomRoles
enum instead of string as role configuration.