AuthenticationHandler error AuthenticationScheme: Bearer was forbidden - asp.net-core

I'm making a small service using ASP.NET Core. The first complicated thing I'm facing now is to authenticate an user into my system.
Let me introduce about my authentication flow:
+)Client -> call (api/account/authorize) -> System check whether client is valid or not -> Send token back to client as him/her is valid.
+) Client -> uses the obtained token -> requests to api/account/filter -> Service validates the token and throw the information back.
I've read some tutorials about JWT, but the response doesn't include enough information as I need. I want :
Throw 401 and a message describes that status code , i.e: "ACCOUNT_DISABLED", "ACCOUNT_PENDING", "ACCOUNT_PERMISSION_INSUFFICIENT", ... not just 401.
Therfore, I implemented my own Authenticate validator:
public class BearerAuthenticationHandler : AuthenticationHandler<BearerAuthenticationOption>
{
#region Properties
/// <summary>
/// Inject dependency service into the handler.
/// </summary>
private readonly JwtTokenSetting _encryptionSetting;
/// <summary>
/// Inject dependency service into the handler.
/// </summary>
private readonly IEncryptionService _encryptionService;
/// <summary>
/// Inject time service to handler.
/// </summary>
private readonly ITimeService _timeService;
private readonly IRepositoryAccount _repositoryAccount;
#endregion
#region Constructors
/// <summary>
/// Initialize an instance of handler with specific dependency injections.
/// </summary>
/// <param name="encryptionSetting"></param>
/// <param name="encryptionService"></param>
/// <param name="timeService"></param>
/// <param name="repositoryAccount"></param>
public BearerAuthenticationHandler(JwtTokenSetting encryptionSetting, IEncryptionService encryptionService, ITimeService timeService, IRepositoryAccount repositoryAccount)
{
_encryptionSetting = encryptionSetting;
_encryptionService = encryptionService;
_timeService = timeService;
_repositoryAccount = repositoryAccount;
}
#endregion
#region Methods
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
#region Token analyzation
// Find the authorization key in request.
var authorizationKey =
Request.Headers.Keys.FirstOrDefault(x => x.Equals("authorization", StringComparison.OrdinalIgnoreCase));
// Authorization key is not found in the request.
if (string.IsNullOrWhiteSpace(authorizationKey))
return AuthenticateResult.Fail("No authorization is found in request header.");
// Find the token in Authorization.
var authorizationValue = Request.Headers[authorizationKey].ToString();
// Authentication scheme prefix.
var authenticationScheme = $"{Options.AuthenticationScheme} ";
// No token has been specified.
if (string.IsNullOrWhiteSpace(authorizationValue) || !authorizationValue.StartsWith(authenticationScheme, StringComparison.OrdinalIgnoreCase))
return AuthenticateResult.Fail("No bearer token is found in request header.");
// Cut the string to obtain bearer token.
var accessToken = authorizationValue.Substring(authenticationScheme.Length);
#endregion
#region Token validation
// Decrypt the token.
var tokenDetailViewModel = _encryptionService.Decrypt<TokenDetailViewModel>(accessToken, _encryptionSetting.Key);
// No detail has been found.
if (tokenDetailViewModel == null)
{
InitializeHttpResponse(Response, HttpStatusCode.Unauthorized, new HttpResponseViewModel
{
Message = "TOKEN_INVALID"
});
return AuthenticateResult.Fail("Token is invalid");
}
// Find the current unix time on server.
var unixTime = _timeService.UtcToUnix(DateTime.UtcNow);
// Token is expired.
if (unixTime > tokenDetailViewModel.Expire)
{
InitializeHttpResponse(Response, HttpStatusCode.Unauthorized, new HttpResponseViewModel
{
Message = "TOKEN_EXPIRED"
});
return AuthenticateResult.Fail("Token is expired");
}
// Account filter construction.
var filterAccountViewModel = new FilterAccountViewModel
{
Email = tokenDetailViewModel.Email,
EmailComparison = TextComparision.Equal,
Password = tokenDetailViewModel.Password,
PasswordComparision = TextComparision.EqualIgnoreCase,
Statuses = new[] { AccountStatus.Active }
};
// Find the first condition statisfied account in the database.
var account = await _repositoryAccount.FindAccountAsync(filterAccountViewModel);
// Account cannot be found in the database.
if (account == null)
{
InitializeHttpResponse(Response, HttpStatusCode.Unauthorized, new HttpResponseViewModel
{
Message = "ACCOUNT_INVALID"
});
return AuthenticateResult.Fail("Account is invalid");
}
#endregion
var claimsIdentity = new ClaimsIdentity();
claimsIdentity.AddClaim(new Claim(nameof(JwtClaim.Email), account.Email));
claimsIdentity.AddClaim(new Claim(nameof(JwtClaim.Status), nameof(account.Status)));
// Update user into context.
var claimPrincipal = new ClaimsPrincipal(claimsIdentity);
// Initialize an authentication ticket.
var authenticationTicket = new AuthenticationTicket(claimPrincipal, new AuthenticationProperties
{
AllowRefresh = true,
ExpiresUtc = DateTime.UtcNow.AddMinutes(30),
IsPersistent = true,
IssuedUtc = DateTime.UtcNow
}, "Bearer");
return AuthenticateResult.Success(authenticationTicket);
}
/// <summary>
/// Initialize an application/json response.
/// </summary>
/// <param name="httpResponse"></param>
/// <param name="httpStatusCode"></param>
/// <param name="httpResponseViewModel"></param>
private void InitializeHttpResponse(HttpResponse httpResponse, HttpStatusCode httpStatusCode, HttpResponseViewModel httpResponseViewModel)
{
// Response must be always application/json.
httpResponse.ContentType = "application/json";
httpResponse.StatusCode = (int)httpStatusCode;
if (httpResponseViewModel == null)
return;
using (var streamWriter = new StreamWriter(httpResponse.Body))
{
streamWriter.AutoFlush = true;
streamWriter.WriteLineAsync(JsonConvert.SerializeObject(httpResponseViewModel));
}
}
#endregion
}
Here is my AccountController:
[Route("api/[controller]")]
public class AccountController : Controller
{
private readonly IRepositoryAccount _repositoryAccount;
private readonly IEncryptionService _encryptionService;
private readonly ITimeService _timeService;
private readonly JwtTokenSetting _jwtTokenSetting;
public AccountController(IRepositoryAccount repositoryAccount, IEncryptionService encryptionService, ITimeService timeService,
IOptions<JwtTokenSetting> jwtTokenSetting)
{
_repositoryAccount = repositoryAccount;
_encryptionService = encryptionService;
_timeService = timeService;
_jwtTokenSetting = jwtTokenSetting.Value;
}
[HttpPost("authorize")]
[AllowAnonymous]
public async Task<IActionResult> Authorize([FromBody] LoginViewModel loginViewModel)
{
// Find the encrypted password of login information.
var filterAccountViewModel = new FilterAccountViewModel();
filterAccountViewModel.Email = loginViewModel.Email;
filterAccountViewModel.EmailComparison = TextComparision.Equal;
filterAccountViewModel.Password = _encryptionService.FindEncryptPassword(loginViewModel.Password);
filterAccountViewModel.PasswordComparision = TextComparision.EqualIgnoreCase;
filterAccountViewModel.Statuses = new[] {AccountStatus.Active};
// Initialize HttpResponseViewModel.
var httpResponseViewModel = new HttpResponseViewModel();
// Find the account.
var account = await _repositoryAccount.FindAccountAsync(filterAccountViewModel);
// Account is not found.
if (account == null)
{
Response.ContentType = "application/json";
using (var streamWriter = new StreamWriter(Response.Body))
{
httpResponseViewModel.Message = "ACCOUNT_INVALID";
await streamWriter.WriteLineAsync(JsonConvert.SerializeObject(httpResponseViewModel));
}
return new UnauthorizedResult();
}
// Initialize token detail.
var tokenDetailViewModel = new TokenDetailViewModel
{
Email = loginViewModel.Email,
Password = filterAccountViewModel.Password,
Expire = _timeService.UtcToUnix(DateTime.UtcNow.AddSeconds(_jwtTokenSetting.Expire))
};
// Initialize token information and throw to client for their future use.
var tokenGeneralViewModel = new TokenGeneralViewModel
{
AccessToken = _encryptionService.Encrypt(tokenDetailViewModel, _jwtTokenSetting.Key),
Expire = _jwtTokenSetting.Expire
};
return Ok(tokenGeneralViewModel);
}
[HttpPost("filter")]
[Authorize(ActiveAuthenticationSchemes = "Bearer")]
public IEnumerable<string> FindAllAccounts()
{
Response.StatusCode = (int)HttpStatusCode.Accepted;
return new[] { "1", "2", "3", "4" };
}
}
When I use the token generated by api/account/authorize to access api/account/filter. An error was thrown to me :
AuthenticationScheme: Bearer was forbidden
Can anyone please tell me why ? Is my implementation the best approach or not ?
Thank you,

Is my implementation the best approach or not ?
I wouldn't do this as you implemented. Because(1 and 3 are just my opinions)
ACCOUNT_DISABLED, ACCOUNT_PENDING,
ACCOUNT_PERMISSION_INSUFFICIENT this statuses doesn't mean that user has to
retype its cridentials.
Even if i want to use 401 with a message, before creating my own
handler implementation, i would consider using jwt bearer events. OnChallenge event seems good to do this(See this answer how to implement).
I think your requirement is related with authorization rather than authentication. So writing a policy
would be better.
To use policy i don't know simple implementation, but here is my attempt:
Authorization Handler:
public class CheckUserRequirement : IAuthorizationRequirement
{
}
public class CheckUserAuthorizationHandler : AuthorizationHandler<CheckUserRequirement>
{
private readonly IHttpContextAccessor _accessor;
public SimpleAuthorizationHandler(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, SimpleRequirement requirement)
{
if(account.isDisabled)
{
_accessor.HttpContext.Response.Headers.Add("error_code", "ACCOUNT_DISABLED");
}
//...
context.Succeed(requirement);
}
}
ConfigureServices:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IAuthorizationHandler, CheckUserAuthorizationHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("CheckUser", policy => { policy.AddRequirements(new CheckUserRequirement()); });
});
And use it:
[Authorize(Policy = "CheckUser")]
public class SomeController
Edit
I had suggested OnChallenge event, but i realized that it is not suitable for your case. See my another answer

Related

.Net Core integration tests with mock keeps returning null

I am trying to be a good boy and write some unit and integration tests but I am slowly giving up. On the controller side, I have:
public class PointController : BaseApiController
{
private readonly IPointL _pointL;
public PointController(IMapper mapper, IPrincipal principal, IPointL pointL) : base(mapper, principal)
{
_pointL = pointL;
}
// GET: api/<PointController>
/// <summary>
/// Get point by id.
/// </summary>
/// <param name="id">Point id.</param>
/// <returns></returns>
[HttpGet("{id}")]
public async Task<PointDTO> GetPointAsync(int id)
{
// Get point.
var pointModel = await _pointL.GetPointAsync(id);
var pointDTO = _mapper.Map<PointDTO>(pointModel);
return pointDTO;
}
}
So the line var pointModel = await _pointL.GetPointAsync(id); in fact calls my logic layer:
public class PointL : BaseL, IPointL
{
private readonly IBaseDAL _baseDAL;
public PointL(IMapper mapper, IPrincipal principal, IBaseDAL baseDAL) : base(mapper, principal)
{
_baseDAL = baseDAL;
}
///<inheritdoc/>
public async Task<PointModel> GetPointAsync(int id)
{
_logger.Info($"Get point with id {id}.");
var pointDBModel = await _baseDAL.GetPointAsync(id);
if (pointDBModel is null)
{
throw new Exception($"Point with id {id} not found.");
}
var pointModel = _mapper.Map<PointModel>(pointDBModel);
return pointModel;
}
}
which drops to the final, DAL layer with the await _baseDAL.GetPointAsync(id);
public partial class BaseDAL
{
///<inheritdoc/>
public async Task<Point> GetPointAsync(int id)
{
_logger.Info($"Get point from database with id {id}.");
using var dbGlistaContext = new GlistaContext(_options);
var point = await dbGlistaContext.Point.FindAsync(id);
return point;
}
}
Now my plan was to write an integration test for the GetPointAsync(int id) method on the controller. I guess, this is the usual way or am I mistaken?
Either way, this is my not working attempt:
private readonly PointController _sut;
private readonly Mock<IPointL> _pointLMock = new Mock<IPointL>();
protected readonly Mock<IMapper> _mapperMock = new Mock<IMapper>();
protected readonly Mock<IPrincipal> _principalMock = new Mock<IPrincipal>();
public PointControllerTests()
{
_sut = new PointController(_mapperMock.Object, _principalMock.Object, _pointLMock.Object);
}
[Fact]
public async System.Threading.Tasks.Task GetPointAsync_ShouldReturnPoint_WhenPointExists()
{
// Arrange.
var pointId = 1;
var pointModel = new PointModel
{
Id = pointId,
Name = "T01",
};
_pointLMock.Setup(x => x.GetPointAsync(pointId))
.ReturnsAsync(pointModel);
// Act.
var point = await _sut.GetPointAsync(pointId); // Keeps returning null no matter what! :(
// Assert.
Assert.Equal(pointId, point.Id);
}
This is for some reason NOT working as mock keeps returning null values! Does anybody have an idea why? I have watched some tutorials but I can't figure out the solution.
EDIT:
I was able to find out that the controller
var pointModel = await _pointL.GetPointAsync(id);
var pointDTO = _mapper.Map<PointDTO>(pointModel);
gets the pointModel, however, the mapping to DTO returns null. Thus I tried setting the mapper mock
var pointDTO = new PointDTO();
mapperMock.Setup(m => m.Map<PointModel, PointDTO>(It.IsAny<PointModel>())).Returns(pointDTO);
However, without any success.
You need to setup the Map method of the mapper mock object.
_mapperMock.Setup(x => x.Map<PointDTO>(It.IsAny<PointDTO>()))
.Returns(...);

IdentityServer4 losing original returnUrl when using External Login server

Unfortunately, given the size of the project, I can’t easily share a reproducible version. However, hopefully what I have below will shed some light on my issue and you’ll see where I made a mistake.
I have two sites, an ASP.Net Core MVC application and a Login server, also ASP.Net Core MVC. Let’s call them http://mvc.mysite.com and http://login.mysite.com. Neither are significantly different from the IdentityServer4 Quickstart #6. The only real difference is that I have implemented an external login provider for AzureAd. My code for that is below.
Scenario 1
Given an internal login flow, where the user uses an internal login page at http://login.mysite.com everything works fine.
User visits http://mvc.mysite.com/clients/client-page-1
User is redirected to http://login.mysite.com/Account/Login
User logs in with correct username/password
User is redirected to http://mvc.mysite.com/clients/client-page-1
Scenario 2
However, if the login server’s AccountController::Login() method determines there is a single ExternalLoginProvider and executes the line “return await ExternalLogin(vm.ExternalLoginScheme, returnUrl);” then the original redirectUrl is lost.
User visits http://mvc.mysite.com/clients/client-page-1
User is redirected to http://login.mysite.com/Account/Login (receiving the output of AccountController::ExternalLogin)
User is redirected to AzureAd External OIDC Provider
User logs in with correct username/password
User is redirected to http://login.mysite.com/Account/ExternalLoginCallback
User is redirected to http://mvc.mysite.com (Notice that the user is redirected to the root of the MVC site instead of /clients/client-page-1)
For Scenario 1:
Given the MVC site
When using the debugger to inspect the Context provided to the OpenIdConnectEvents (e.g. OnMessageReceived, OnUserInformationReceived, etc.)
Then all Contexts have a Properties object that contains a RedirectUri == “http://mvc.mysite.com/clients/client-page-1”
For Scenario 2:
Given the MVC site
When using the debugger to inspect the Context provided to the OpenIdConnectEvents (e.g. OnMessageReceived, OnUserInformationReceived, etc.)
Then all Contexts have a Properties object that contains a RedirectUri == “http://mvc.mysite.com” (missing the /client.client-page-1)
In my login server’s Startup.cs I have added this to ConfigureServices:
services.AddAuthentication()
.AddAzureAd(options =>
{
Configuration.Bind("AzureAd", options);
AzureAdOptions.Settings = options;
});
The implementation of AddAzureAd is as follows: (You’ll see options objects handed around, I have replaced all uses of options with constant values except for ClientId and ClientSecret).
public static class AzureAdAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
builder.AddOpenIdConnect("AzureAd", "Azure AD", options =>
{
var opts = new AzureAdOptions();
configureOptions(opts);
var config = new ConfigureAzureOptions(opts);
config.Configure(options);
});
return builder;
}
private class ConfigureAzureOptions : IConfigureNamedOptions<OpenIdConnectOptions>
{
private readonly AzureAdOptions _azureOptions;
public ConfigureAzureOptions(AzureAdOptions azureOptions)
{
_azureOptions = azureOptions;
}
public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions) : this(azureOptions.Value) {}
public void Configure(string name, OpenIdConnectOptions options)
{
Configure(options);
}
public void Configure(OpenIdConnectOptions options)
{
options.ClientId = _azureOptions.ClientId;
options.Authority = "https://login.microsoftonline.com/common"; //_azureOptions.Authority;
options.UseTokenLifetime = true;
options.CallbackPath = "/signin-oidc"; // _azureOptions.CallbackPath;
options.RequireHttpsMetadata = false; // true in production // _azureOptions.RequireHttps;
options.ClientSecret = _azureOptions.ClientSecret;
// Add code for hybridflow
options.ResponseType = "id_token code";
options.TokenValidationParameters = new IdentityModel.Tokens.TokenValidationParameters
{
// instead of using the default validation (validating against a single issuer value, as we do in line of business apps),
// we inject our own multitenant validation logic
ValidateIssuer = false,
};
// Subscribing to the OIDC events
options.Events.OnAuthorizationCodeReceived = OnAuthorizationCodeReceived;
options.Events.OnAuthenticationFailed = OnAuthenticationFailed;
}
/// <summary>
/// Redeems the authorization code by calling AcquireTokenByAuthorizationCodeAsync in order to ensure
/// that the cache has a token for the signed-in user.
/// </summary>
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
string userObjectId = (context.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
var authContext = new AuthenticationContext(context.Options.Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
var credential = new ClientCredential(context.Options.ClientId, context.Options.ClientSecret);
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(context.TokenEndpointRequest.Code,
new Uri(context.TokenEndpointRequest.RedirectUri, UriKind.RelativeOrAbsolute), credential, context.Options.Resource);
// Notify the OIDC middleware that we already took care of code redemption.
context.HandleCodeRedemption(authResult.AccessToken, context.ProtocolMessage.IdToken);
}
private Task OnAuthenticationFailed(AuthenticationFailedContext context)
{
throw context.Exception;
}
}
}
public class NaiveSessionCache : TokenCache
{
private static readonly object FileLock = new object();
string UserObjectId = string.Empty;
string CacheId = string.Empty;
ISession Session = null;
public NaiveSessionCache(string userId, ISession session)
{
UserObjectId = userId;
CacheId = UserObjectId + "_TokenCache";
Session = session;
this.AfterAccess = AfterAccessNotification;
this.BeforeAccess = BeforeAccessNotification;
Load();
}
public void Load()
{
lock (FileLock)
this.Deserialize(Session.Get(CacheId));
}
public void Persist()
{
lock (FileLock)
{
// reflect changes in the persistent store
Session.Set(CacheId, this.Serialize());
// once the write operation took place, restore the HasStateChanged bit to false
this.HasStateChanged = false;
}
}
// Empties the persistent store.
public override void Clear()
{
base.Clear();
Session.Remove(CacheId);
}
public override void DeleteItem(TokenCacheItem item)
{
base.DeleteItem(item);
Persist();
}
// Triggered right before ADAL needs to access the cache.
// Reload the cache from the persistent store in case it changed since the last access.
void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
Load();
}
// Triggered right after ADAL accessed the cache.
void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (this.HasStateChanged)
Persist();
}
}

openid connect - identifying tenant during login

I have a multi-tenant (single database) application which allows for same username/email across different tenants.
At the time of login (Implicit flow) how can I identify the tenant? I thought of following possibilities:
At the time of registration ask the user for account slug (company/tenant slug) and during login user should provide the slug along with username and password.
But there is no parameter in open id request to send the slug.
Create an OAuth application at the time of registration and use slug as client_id. At the time of login pass slug in client_id, which I will use to fetch the tenant Id and proceed further to validate the user.
Is this approach fine?
Edit:
Also tried making slug part of route param
.EnableTokenEndpoint("/connect/{slug}/token");
but openiddict doesn't support that.
Edit: this answer was updated to use OpenIddict 3.x.
The approach suggested by McGuire will work with OpenIddict (you can access the acr_values property via OpenIddictRequest.AcrValues) but it's not the recommended option (it's not ideal from a security perspective: since the issuer is the same for all the tenants, they end up sharing the same signing keys).
Instead, consider running an issuer per tenant. For that, you have at least 2 options:
Give OrchardCore's OpenID module a try: it's based on OpenIddict and natively supports multi-tenancy. It's still in beta but it's actively developed.
Override the options monitor used by OpenIddict to use per-tenant options.
Here's a simplified example of the second option, using a custom monitor and path-based tenant resolution:
Implement your tenant resolution logic. E.g:
public class TenantProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
public TenantProvider(IHttpContextAccessor httpContextAccessor)
=> _httpContextAccessor = httpContextAccessor;
public string GetCurrentTenant()
{
// This sample uses the path base as the tenant.
// You can replace that by your own logic.
string tenant = _httpContextAccessor.HttpContext.Request.PathBase;
if (string.IsNullOrEmpty(tenant))
{
tenant = "default";
}
return tenant;
}
}
public void Configure(IApplicationBuilder app)
{
app.Use(next => context =>
{
// This snippet uses a hardcoded resolution logic.
// In a real world app, you'd want to customize that.
if (context.Request.Path.StartsWithSegments("/fabrikam", out PathString path))
{
context.Request.PathBase = "/fabrikam";
context.Request.Path = path;
}
return next(context);
});
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseStatusCodePagesWithReExecute("/error");
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(options =>
{
options.MapControllers();
options.MapDefaultControllerRoute();
});
}
Implement a custom IOptionsMonitor<OpenIddictServerOptions>:
public class OpenIddictServerOptionsProvider : IOptionsMonitor<OpenIddictServerOptions>
{
private readonly ConcurrentDictionary<(string Name, string Tenant), Lazy<OpenIddictServerOptions>> _cache;
private readonly IOptionsFactory<OpenIddictServerOptions> _optionsFactory;
private readonly TenantProvider _tenantProvider;
public OpenIddictServerOptionsProvider(
IOptionsFactory<OpenIddictServerOptions> optionsFactory,
TenantProvider tenantProvider)
{
_cache = new ConcurrentDictionary<(string, string), Lazy<OpenIddictServerOptions>>();
_optionsFactory = optionsFactory;
_tenantProvider = tenantProvider;
}
public OpenIddictServerOptions CurrentValue => Get(Options.DefaultName);
public OpenIddictServerOptions Get(string name)
{
var tenant = _tenantProvider.GetCurrentTenant();
Lazy<OpenIddictServerOptions> Create() => new(() => _optionsFactory.Create(name));
return _cache.GetOrAdd((name, tenant), _ => Create()).Value;
}
public IDisposable OnChange(Action<OpenIddictServerOptions, string> listener) => null;
}
Implement a custom IConfigureNamedOptions<OpenIddictServerOptions>:
public class OpenIddictServerOptionsInitializer : IConfigureNamedOptions<OpenIddictServerOptions>
{
private readonly TenantProvider _tenantProvider;
public OpenIddictServerOptionsInitializer(TenantProvider tenantProvider)
=> _tenantProvider = tenantProvider;
public void Configure(string name, OpenIddictServerOptions options) => Configure(options);
public void Configure(OpenIddictServerOptions options)
{
var tenant = _tenantProvider.GetCurrentTenant();
// Resolve the signing credentials associated with the tenant (in a real world application,
// the credentials would be retrieved from a persistent storage like a database or a key vault).
options.SigningCredentials.Add(tenant switch
{
"fabrikam" => new(new RsaSecurityKey(RSA.Create(keySizeInBits: 2048)), SecurityAlgorithms.RsaSha256),
_ => new(new RsaSecurityKey(RSA.Create(keySizeInBits: 2048)), SecurityAlgorithms.RsaSha256)
});
// Resolve the encryption credentials associated with the tenant (in a real world application,
// the credentials would be retrieved from a persistent storage like a database or a key vault).
options.EncryptionCredentials.Add(tenant switch
{
"fabrikam" => new(new RsaSecurityKey(RSA.Create(keySizeInBits: 2048)),
SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512),
_ => new(new RsaSecurityKey(RSA.Create(keySizeInBits: 2048)),
SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512)
});
// Other tenant-specific options can be registered here.
}
}
Register the services in your DI container:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
// Enable the authorization, device, introspection,
// logout, token, userinfo and verification endpoints.
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetDeviceEndpointUris("/connect/device")
.SetIntrospectionEndpointUris("/connect/introspect")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo")
.SetVerificationEndpointUris("/connect/verify");
// Note: this sample uses the code, device code, password and refresh token flows, but you
// can enable the other flows if you need to support implicit or client credentials.
options.AllowAuthorizationCodeFlow()
.AllowDeviceCodeFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow();
// Mark the "email", "profile", "roles" and "demo_api" scopes as supported scopes.
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles, "demo_api");
// Force client applications to use Proof Key for Code Exchange (PKCE).
options.RequireProofKeyForCodeExchange();
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
options.UseAspNetCore()
.EnableStatusCodePagesIntegration()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableVerificationEndpointPassthrough();
});
services.AddSingleton<TenantProvider>();
services.AddSingleton<IOptionsMonitor<OpenIddictServerOptions>, OpenIddictServerOptionsProvider>();
services.AddSingleton<IConfigureOptions<OpenIddictServerOptions>, OpenIddictServerOptionsInitializer>();
}
To confirm this works correctly, navigate to https://localhost:[port]/fabrikam/.well-known/openid-configuration (you should get a JSON response with the OpenID Connect metadata).
For anyone who's interested in an alternative approach (more of an extension) to Kevin Chalet's accepted answer look at the pattern described here using a custom implementation of IOptions<TOption> as MultiTenantOptionsManager<TOptions> https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/master/docs/Options.md
The authentication sample for the same pattern is here https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/master/docs/Authentication.md
The full source code for the implemenation is here https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/7bc72692b0f509e0348fe17dd3248d35f4f2b52c/src/Finbuckle.MultiTenant.Core/Options/MultiTenantOptionsManager.cs
The trick is using a custom IOptionsMonitorCache that is tenant aware and always returns a tenant scoped result https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/7bc72692b0f509e0348fe17dd3248d35f4f2b52c/src/Finbuckle.MultiTenant.Core/Options/MultiTenantOptionsCache.cs
internal class MultiTenantOptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new()
{
private readonly IOptionsFactory<TOptions> _factory;
private readonly IOptionsMonitorCache<TOptions> _cache; // Note: this is a private cache
/// <summary>
/// Initializes a new instance with the specified options configurations.
/// </summary>
/// <param name="factory">The factory to use to create options.</param>
public MultiTenantOptionsManager(IOptionsFactory<TOptions> factory, IOptionsMonitorCache<TOptions> cache)
{
_factory = factory;
_cache = cache;
}
public TOptions Value
{
get
{
return Get(Microsoft.Extensions.Options.Options.DefaultName);
}
}
public virtual TOptions Get(string name)
{
name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
// Store the options in our instance cache.
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
public void Reset()
{
_cache.Clear();
}
}
public class MultiTenantOptionsCache<TOptions> : IOptionsMonitorCache<TOptions> where TOptions : class
{
private readonly IMultiTenantContextAccessor multiTenantContextAccessor;
// The object is just a dummy because there is no ConcurrentSet<T> class.
//private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, object>> _adjustedOptionsNames =
// new ConcurrentDictionary<string, ConcurrentDictionary<string, object>>();
private readonly ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>> map = new ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>>();
public MultiTenantOptionsCache(IMultiTenantContextAccessor multiTenantContextAccessor)
{
this.multiTenantContextAccessor = multiTenantContextAccessor ?? throw new ArgumentNullException(nameof(multiTenantContextAccessor));
}
/// <summary>
/// Clears all cached options for the current tenant.
/// </summary>
public void Clear()
{
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
cache.Clear();
}
/// <summary>
/// Clears all cached options for the given tenant.
/// </summary>
/// <param name="tenantId">The Id of the tenant which will have its options cleared.</param>
public void Clear(string tenantId)
{
tenantId = tenantId ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
cache.Clear();
}
/// <summary>
/// Clears all cached options for all tenants and no tenant.
/// </summary>
public void ClearAll()
{
foreach(var cache in map.Values)
cache.Clear();
}
/// <summary>
/// Gets a named options instance for the current tenant, or adds a new instance created with createOptions.
/// </summary>
/// <param name="name">The options name.</param>
/// <param name="createOptions">The factory function for creating the options instance.</param>
/// <returns>The existing or new options instance.</returns>
public TOptions GetOrAdd(string name, Func<TOptions> createOptions)
{
if (createOptions == null)
{
throw new ArgumentNullException(nameof(createOptions));
}
name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
return cache.GetOrAdd(name, createOptions);
}
/// <summary>
/// Tries to adds a new option to the cache for the current tenant.
/// </summary>
/// <param name="name">The options name.</param>
/// <param name="options">The options instance.</param>
/// <returns>True if the options was added to the cache for the current tenant.</returns>
public bool TryAdd(string name, TOptions options)
{
name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
return cache.TryAdd(name, options);
}
/// <summary>
/// Try to remove an options instance for the current tenant.
/// </summary>
/// <param name="name">The options name.</param>
/// <returns>True if the options was removed from the cache for the current tenant.</returns>
public bool TryRemove(string name)
{
name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
return cache.TryRemove(name);
}
}
The advantage is you don't have to extend every type of IOption<TOption>.
It can be hooked up as shown in the example https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/3c94ab2848758de7c9d0154aeddd4820dd545fbf/src/Finbuckle.MultiTenant.Core/DependencyInjection/MultiTenantBuilder.cs#L71
private static MultiTenantOptionsManager<TOptions> BuildOptionsManager<TOptions>(IServiceProvider sp) where TOptions : class, new()
{
var cache = ActivatorUtilities.CreateInstance(sp, typeof(MultiTenantOptionsCache<TOptions>));
return (MultiTenantOptionsManager<TOptions>)
ActivatorUtilities.CreateInstance(sp, typeof(MultiTenantOptionsManager<TOptions>), new[] { cache });
}
Using it https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/3c94ab2848758de7c9d0154aeddd4820dd545fbf/src/Finbuckle.MultiTenant.Core/DependencyInjection/MultiTenantBuilder.cs#L43
public static void WithPerTenantOptions<TOptions>(Action<TOptions, TenantInfo> tenantInfo) where TOptions : class, new()
{
// Other required services likes custom options factory, see the linked example above for full code
Services.TryAddScoped<IOptionsSnapshot<TOptions>>(sp => BuildOptionsManager<TOptions>(sp));
Services.TryAddSingleton<IOptions<TOptions>>(sp => BuildOptionsManager<TOptions>(sp));
}
Every time IOptions<TOption>.Value is called it looks up the multi tenant aware cache to retrieve it. So you can conveniently use it in singletons like the IAuthenticationSchemeProvider as well.
Now you can register your tenant specific OpenIddictServerOptionsProvider options same as the accepted answer.
You're on the right track with the OAuth process. When you register the OpenID Connect scheme in your client web app's startup code, add a handler for the OnRedirectToIdentityProvider event and use that to add your "slug" value as the "tenant" ACR value (something OIDC calls the "Authentication Context Class Reference").
Here's an example of how you'd pass it to the server:
.AddOpenIdConnect("tenant", options =>
{
options.CallbackPath = "/signin-tenant";
// other options omitted
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async context =>
{
string slug = await GetCurrentTenantAsync();
context.ProtocolMessage.AcrValues = $"tenant:{slug}";
}
};
}
You didn't specify what sort of server this is going to, but ACR (and the "tenant" value) are standard parts of OIDC. If you're using Identity Server 4, you could just inject the Interaction Service into the class processing the login and read the Tenant property, which is automatically parsed out of the ACR values for you. This example is non-working code for several reasons, but it demonstrates the important parts:
public class LoginModel : PageModel
{
private readonly IIdentityServerInteractionService interaction;
public LoginModel(IIdentityServerInteractionService interaction)
{
this.interaction = interaction;
}
public async Task<IActionResult> PostEmailPasswordLoginAsync()
{
var context = await interaction.GetAuthorizationContextAsync(returnUrl);
if(context != null)
{
var slug = context.Tenant;
// etc.
}
}
}
In terms of identifying the individual user accounts, your life will be a lot easier if you stick to the OIDC standard of using "subject ID" as the unique user ID. (In other words, make that the key where you store your user data like the tenant "slug", the user email address, password salt and hash, etc.)

Override AuthorizeAttribute in ASP.Net Core and respond Json status

I'm moving from ASP.Net Framework to ASP.Net Core.
In ASP.Net Framework with Web API 2 project, I can customize AuthorizeAttribute like this :
public class ApiAuthorizeAttribute : AuthorizationFilterAttribute
{
#region Methods
/// <summary>
/// Override authorization event to do custom authorization.
/// </summary>
/// <param name="httpActionContext"></param>
public override void OnAuthorization(HttpActionContext httpActionContext)
{
// Retrieve email and password.
var accountEmail =
httpActionContext.Request.Headers.Where(
x =>
!string.IsNullOrEmpty(x.Key) &&
x.Key.Equals("Email"))
.Select(x => x.Value.FirstOrDefault())
.FirstOrDefault();
// Retrieve account password.
var accountPassword =
httpActionContext.Request.Headers.Where(
x =>
!string.IsNullOrEmpty(x.Key) &&
x.Key.Equals("Password"))
.Select(x => x.Value.FirstOrDefault()).FirstOrDefault();
// Account view model construction.
var filterAccountViewModel = new FilterAccountViewModel();
filterAccountViewModel.Email = accountEmail;
filterAccountViewModel.Password = accountPassword;
filterAccountViewModel.EmailComparision = TextComparision.Equal;
filterAccountViewModel.PasswordComparision = TextComparision.Equal;
// Find the account.
var account = RepositoryAccount.FindAccount(filterAccountViewModel);
// Account is not found.
if (account == null)
{
// Treat the account as unthorized.
httpActionContext.Response = httpActionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
return;
}
// Role is not defined which means the request is allowed.
if (_roles == null)
return;
// Role is not allowed
if (!_roles.Any(x => x == account.Role))
{
// Treat the account as unthorized.
httpActionContext.Response = httpActionContext.Request.CreateResponse(HttpStatusCode.Forbidden);
return;
}
// Store the requester information in action argument.
httpActionContext.ActionArguments["Account"] = account;
}
#endregion
#region Properties
/// <summary>
/// Repository which provides function to access account database.
/// </summary>
public IRepositoryAccount RepositoryAccount { get; set; }
/// <summary>
/// Which role can be allowed to access server.
/// </summary>
private readonly byte[] _roles;
#endregion
#region Constructor
/// <summary>
/// Initialize instance with default settings.
/// </summary>
public ApiAuthorizeAttribute()
{
}
/// <summary>
/// Initialize instance with allowed role.
/// </summary>
/// <param name="roles"></param>
public ApiAuthorizeAttribute(byte[] roles)
{
_roles = roles;
}
#endregion
}
In my customized AuthorizeAttribute, I can check whether account is valid or not and return HttpStatusCode with message to client.
I'm trying to do the samething in ASP.Net Core, but no OnAuthorization for me to override.
How can I achieve the same thing as in ASP.Net Framework ?
Thank you,
You're approaching this incorrectly. It never was really encouraged to write custom attributes for this, or to extend existing. With ASP.NET Core roles are still apart of the system for backwards compatibility but they are now also discouraged.
There is a great 2 part series on some of the driving architecture changes and the way that this is and should be utilized found here. If you want to still rely on roles you can do so, but I would suggest using policies.
To wire a policy do the following:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy(nameof(Policy.Account),
policy => policy.Requirements.Add(new AccountRequirement()));
});
services.AddSingleton<IAuthorizationHandler, AccountHandler>();
}
I created a Policy enum for convenience.
public enum Policy { Account };
Decorate entry points as such:
[
HttpPost,
Authorize(Policy = nameof(Policy.Account))
]
public async Task<IActionResult> PostSomething([FromRoute] blah)
{
}
The AccountRequirement is just a placeholder, it needs to implement the IAuthorizationRequirement interface.
public class AccountRequirement: IAuthorizationRequirement { }
Now we simply need to create a handler and wire this up for DI.
public class AccountHandler : AuthorizationHandler<AccountRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
AccountRequirement requirement)
{
// Your logic here... or anything else you need to do.
if (context.User.IsInRole("fooBar"))
{
// Call 'Succeed' to mark current requirement as passed
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Additional Resources
ASP.NET Core Security -- All the things
My comment looks bad as a comment so I post an answer but only useful if you use MVC:
// don't forget this
services.AddSingleton<IAuthorizationHandler, MyCustomAuthorizationHandler>();
services
.AddMvc(config => { var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser() .AddRequirements(new[] { new MyCustomRequirement() })
.Build(); config.Filters.Add(new AuthorizeFilter(policy)); })
I also noticed that async keyword is superfluous for "HandleRequirementAsync" signature, in question code. And I guess that returning Task.CompletedTask could be good.

Custom OAuth client in MVC4 / DotNetOpenAuth - missing access token secret

I'm currently working on implementing a Dropbox OAuth client for my application. It's been a fairly painless process until I hit the end. Once I've authorized, when I attempt to access user data I get a 401 back from Dropbox about the token being invalid. I asked on the Dropbox forums and it looks like my request is missing the access_token_secret that Dropbox returns back. I was able to use Fiddler to dig out the secret and add it to my request url and it worked fine, so that's definitely the issue. So why doesn't DotNetOpenAuth return back the access token secret when it returns the access token?
For reference, my code:
public class DropboxClient : OAuthClient
{
public static readonly ServiceProviderDescription DropboxServiceDescription = new ServiceProviderDescription
{
RequestTokenEndpoint = new MessageReceivingEndpoint("https://api.dropbox.com/1/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://www.dropbox.com/1/oauth/authorize", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
AccessTokenEndpoint = new MessageReceivingEndpoint("https://api.dropbox.com/1/oauth/access_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new PlaintextSigningBindingElement() }
};
public DropboxClient(string consumerKey, string consumerSecret) :
this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager())
{
}
public DropboxClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager) :
base("dropbox", DropboxServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{
}
protected override DotNetOpenAuth.AspNet.AuthenticationResult VerifyAuthenticationCore(DotNetOpenAuth.OAuth.Messages.AuthorizedTokenResponse response)
{
var profileEndpoint = new MessageReceivingEndpoint("https://api.dropbox.com/1/account/info", HttpDeliveryMethods.GetRequest);
HttpWebRequest request = this.WebWorker.PrepareAuthorizedRequest(profileEndpoint, response.AccessToken);
try
{
using (WebResponse profileResponse = request.GetResponse())
{
using (Stream profileResponseStream = profileResponse.GetResponseStream())
{
using (StreamReader reader = new StreamReader(profileResponseStream))
{
string jsonText = reader.ReadToEnd();
JavaScriptSerializer jss = new JavaScriptSerializer();
dynamic jsonData = jss.DeserializeObject(jsonText);
Dictionary<string, string> extraData = new Dictionary<string, string>();
extraData.Add("displayName", jsonData.display_name ?? "Unknown");
extraData.Add("userId", jsonData.uid ?? "Unknown");
return new DotNetOpenAuth.AspNet.AuthenticationResult(true, ProviderName, extraData["userId"], extraData["displayName"], extraData);
}
}
}
}
catch (WebException ex)
{
using (Stream s = ex.Response.GetResponseStream())
{
using (StreamReader sr = new StreamReader(s))
{
string body = sr.ReadToEnd();
return new DotNetOpenAuth.AspNet.AuthenticationResult(new Exception(body, ex));
}
}
}
}
}
I found your question when I was searching for solution to a similar problem. I solved it by making 2 new classes, which you can read about in this coderwall post.
I'll also copy and paste the full post here:
DotNetOpenAuth.AspNet 401 Unauthorized Error and Persistent Access Token Secret Fix
When designing QuietThyme, our Cloud Ebook Manager, we knew that everyone hates creating new accounts just as much as we do. We started looking for OAuth and OpenId libraries that we could leverage to allow for social login. We ended up using the DotNetOpenAuth.AspNet library for user authentication, because it supports Microsoft, Twitter, Facebook, LinkedIn and Yahoo, and many others right out of the bow. While we had some issues setting it all up, in the end we only needed to do a few small customizations to get most of it working (described in a previous coderwall post). We noticed that, unlike all the others, the LinkedIn client would not authenticate, returning a 401 Unauthorized Error from DotNetOpenAuth. It quickly became apparent that this was due to a signature issue, and after looking at the source we were able to determine that the retrieved AccessToken secret is not being used with the authenticated profile info request.
It acutally makes sense, the reason that OAuthClient class doesn't include the retrieved access token secret is that it's normally not needed for authentication purposes, which is the primary purpose of the ASP.NET OAuth library.
We needed to make authenticated requests against the api, after the user has logged in, to retrieve some standard profile information, including email address and full name. We were able to solve this issue by making use of an InMemoryOAuthTokenManager temporarily.
public class LinkedInCustomClient : OAuthClient
{
private static XDocument LoadXDocumentFromStream(Stream stream)
{
var settings = new XmlReaderSettings
{
MaxCharactersInDocument = 65536L
};
return XDocument.Load(XmlReader.Create(stream, settings));
}
/// Describes the OAuth service provider endpoints for LinkedIn.
private static readonly ServiceProviderDescription LinkedInServiceDescription =
new ServiceProviderDescription
{
AccessTokenEndpoint =
new MessageReceivingEndpoint("https://api.linkedin.com/uas/oauth/accessToken",
HttpDeliveryMethods.PostRequest),
RequestTokenEndpoint =
new MessageReceivingEndpoint("https://api.linkedin.com/uas/oauth/requestToken?scope=r_basicprofile+r_emailaddress",
HttpDeliveryMethods.PostRequest),
UserAuthorizationEndpoint =
new MessageReceivingEndpoint("https://www.linkedin.com/uas/oauth/authorize",
HttpDeliveryMethods.PostRequest),
TamperProtectionElements =
new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
//ProtocolVersion = ProtocolVersion.V10a
};
private string ConsumerKey { get; set; }
private string ConsumerSecret { get; set; }
public LinkedInCustomClient(string consumerKey, string consumerSecret)
: this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager()) { }
public LinkedInCustomClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager)
: base("linkedIn", LinkedInServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{
ConsumerKey = consumerKey;
ConsumerSecret = consumerSecret;
}
//public LinkedInCustomClient(string consumerKey, string consumerSecret) :
// base("linkedIn", LinkedInServiceDescription, consumerKey, consumerSecret) { }
/// Check if authentication succeeded after user is redirected back from the service provider.
/// The response token returned from service provider authentication result.
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
Justification = "We don't care if the request fails.")]
protected override AuthenticationResult VerifyAuthenticationCore(AuthorizedTokenResponse response)
{
// See here for Field Selectors API http://developer.linkedin.com/docs/DOC-1014
const string profileRequestUrl =
"https://api.linkedin.com/v1/people/~:(id,first-name,last-name,headline,industry,summary,email-address)";
string accessToken = response.AccessToken;
var profileEndpoint =
new MessageReceivingEndpoint(profileRequestUrl, HttpDeliveryMethods.GetRequest);
try
{
InMemoryOAuthTokenManager imoatm = new InMemoryOAuthTokenManager(ConsumerKey, ConsumerSecret);
imoatm.ExpireRequestTokenAndStoreNewAccessToken(String.Empty, String.Empty, accessToken, (response as ITokenSecretContainingMessage).TokenSecret);
WebConsumer w = new WebConsumer(LinkedInServiceDescription, imoatm);
HttpWebRequest request = w.PrepareAuthorizedRequest(profileEndpoint, accessToken);
using (WebResponse profileResponse = request.GetResponse())
{
using (Stream responseStream = profileResponse.GetResponseStream())
{
XDocument document = LoadXDocumentFromStream(responseStream);
string userId = document.Root.Element("id").Value;
string firstName = document.Root.Element("first-name").Value;
string lastName = document.Root.Element("last-name").Value;
string userName = firstName + " " + lastName;
string email = String.Empty;
try
{
email = document.Root.Element("email-address").Value;
}
catch(Exception)
{
}
var extraData = new Dictionary<string, string>();
extraData.Add("accesstoken", accessToken);
extraData.Add("name", userName);
extraData.AddDataIfNotEmpty(document, "headline");
extraData.AddDataIfNotEmpty(document, "summary");
extraData.AddDataIfNotEmpty(document, "industry");
if(!String.IsNullOrEmpty(email))
{
extraData.Add("email",email);
}
return new AuthenticationResult(
isSuccessful: true, provider: this.ProviderName, providerUserId: userId, userName: userName, extraData: extraData);
}
}
}
catch (Exception exception)
{
return new AuthenticationResult(exception);
}
}
}
Here's the section that has changed from the base LinkedIn client written by Microsoft.
InMemoryOAuthTokenManager imoatm = new InMemoryOAuthTokenManager(ConsumerKey, ConsumerSecret);
imoatm.ExpireRequestTokenAndStoreNewAccessToken(String.Empty, String.Empty, accessToken, (response as ITokenSecretContainingMessage).TokenSecret);
WebConsumer w = new WebConsumer(LinkedInServiceDescription, imoatm);
HttpWebRequest request = w.PrepareAuthorizedRequest(profileEndpoint, accessToken);
Unfortunately, the IOAuthTOkenManger.ReplaceRequestTokenWithAccessToken(..) method does not get executed until after the VerifyAuthentication() method returns, so we instead have to create a new TokenManager and and create a WebConsumer and HttpWebRequest using the AccessToken credentials we just retrieved.
This solves our simple 401 Unauthorized issue.
Now what happens if you would like to persist the AccessToken credentials after the authentication process? This could be useful for a DropBox client for instance, where you would like to sync files to a user's DropBox asyncronously. The issue goes back to the way the AspNet library was written, it was assumed that DotNetOpenAuth would only be used for user authethentication, not as a basis for futher OAuth api calls. Thankfully the fix was fairly simple, all I had to do was modify the base AuthetnicationOnlyCookieOAuthTokenManger so that the ReplaceRequestTokenWithAccessToken(..) method stored the new AccessToken key and secrets.
/// <summary>
/// Stores OAuth tokens in the current request's cookie
/// </summary>
public class PersistentCookieOAuthTokenManagerCustom : AuthenticationOnlyCookieOAuthTokenManager
{
/// <summary>
/// Key used for token cookie
/// </summary>
private const string TokenCookieKey = "OAuthTokenSecret";
/// <summary>
/// Primary request context.
/// </summary>
private readonly HttpContextBase primaryContext;
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationOnlyCookieOAuthTokenManager"/> class.
/// </summary>
public PersistentCookieOAuthTokenManagerCustom() : base()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationOnlyCookieOAuthTokenManager"/> class.
/// </summary>
/// <param name="context">The current request context.</param>
public PersistentCookieOAuthTokenManagerCustom(HttpContextBase context) : base(context)
{
this.primaryContext = context;
}
/// <summary>
/// Gets the effective HttpContext object to use.
/// </summary>
private HttpContextBase Context
{
get
{
return this.primaryContext ?? new HttpContextWrapper(HttpContext.Current);
}
}
/// <summary>
/// Replaces the request token with access token.
/// </summary>
/// <param name="requestToken">The request token.</param>
/// <param name="accessToken">The access token.</param>
/// <param name="accessTokenSecret">The access token secret.</param>
public new void ReplaceRequestTokenWithAccessToken(string requestToken, string accessToken, string accessTokenSecret)
{
//remove old requestToken Cookie
//var cookie = new HttpCookie(TokenCookieKey)
//{
// Value = string.Empty,
// Expires = DateTime.UtcNow.AddDays(-5)
//};
//this.Context.Response.Cookies.Set(cookie);
//Add new AccessToken + secret Cookie
StoreRequestToken(accessToken, accessTokenSecret);
}
}
Then to use this PersistentCookieOAuthTokenManager all you need to do is modify your DropboxClient constructor, or any other client where you would like to persist the AccessToken Secret
public DropBoxCustomClient(string consumerKey, string consumerSecret)
: this(consumerKey, consumerSecret, new PersistentCookieOAuthTokenManager()) { }
public DropBoxCustomClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager)
: base("dropBox", DropBoxServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{}
The reason that OAuthClient class doesn't include access token secret is that it's normally not needed for authentication purpose, which is the primary purpose of the ASP.NET OAuth library.
That said, if you want to retrieve the access token secret in your case, you can override the VerifyAuthentication() method, instead of VerifyAuthenticationCore() like you are doing above. Inside VerifyAuthentication(), you can call WebWorker.ProcessUserAuthorization() to validation the login and from the returned AuthorizedTokenResponse object, you have access to the token secret.
After doing some digging, I was able to solve this by changing my constructor logic as follows:
public DropboxClient(string consumerKey, string consumerSecret) :
this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager())
{
}
public DropboxClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager) :
base("dropbox", DropboxServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{
}
becomes
public DropboxClient(string consumerKey, string consumerSecret) :
base("dropbox", DropboxServiceDescription, consumerKey, consumerSecret)
{
}
Digging through the DNOA source shows that if you construct an OAuthClient (my base class) with just the consumer key and secret, it uses the InMemoryOAuthTokenManager instead of the SimpleConsumerTokenManager. I don't know why, but now my access token secret is properly appended to my signature in the authorized request and everything works. Hopefully this helps someone else. In the meantime, I'll likely clean this up for a blog post since there is zero guidance on the net (that I can find) for doing this.
EDIT: I'm going to undo my answer since, as a colleague pointed out, this will take care of one request, but now that I'm using the in-memory manager, that will flush once I round trip fully back to the browser (I'm assuming). So I think the root issue here is that I need to get the access token secret, which I still haven't seen how to do.
As for your original question that the secret is not provided in response--the secret is right there when you get the response in the verifyAuthenticationCore function. You get both of them like this:
string token = response.AccessToken; ;
string secret = (response as ITokenSecretContainingMessage).TokenSecret;