I develop two separated applications: MVC and WebAPI. On some pages of MVC application I perform ajax requests to WebAPI. Furthermore, I use IdentityServer3 as an authentication/authorization framework.
I've already implemented cookie-based authentication for MVC part and token-based for WebAPI basing on tutorials/samples published on GitHub. Each of them works as intended, but user has to log in twice (separately in MVC and WebAPI), which seems to be reasonable because I've used different authentication types.
Is it possible to use IdentityServer3 in a way that user is required to log in once? I'm wondering if it's a good idea to generate access token by MVC app (after cookie-based authorization) and provide it to JavaScript part of application (the token would be used during ajax calls). I think that this solution allows to avoid double signing in. I've read a lot of posts about similar problems, but they haven't given unambiguous answer.
Edit:
I've followed Paul Taylor's suggestion to use "Hybrid Flow" and I've found a couple of samples which illustrate how to implement it (among other things this tutorial), but I cannot figure out how to perform valid ajax requests to WebAPI. Currently, I get 401 Unauthorized error, though HTTP header Authorization: Bearer <access token> is set for all ajax requests.
IdentityServer project
Scopes:
var scopes = new List<Scope>
{
StandardScopes.OfflineAccess,
new Scope
{
Enabled = true,
Name = "roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim(IdentityServer3.Core.Constants.ClaimTypes.Role, true)
}
},
new Scope
{
Enabled = true,
DisplayName = "Web API",
Name = "api",
ScopeSecrets = new List<Secret>
{
new Secret("secret".Sha256())
},
Claims = new List<ScopeClaim>
{
new ScopeClaim(IdentityServer3.Core.Constants.ClaimTypes.Role, true)
},
Type = ScopeType.Resource
}
};
scopes.AddRange(StandardScopes.All);
Client:
new Client
{
ClientName = "MVC Client",
ClientId = "mvc",
Flow = Flows.Hybrid,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = new List<string>
{
Constants.StandardScopes.OpenId,
Constants.StandardScopes.Profile,
Constants.StandardScopes.Email,
Constants.StandardScopes.Roles,
Constants.StandardScopes.Address,
Constants.StandardScopes.OfflineAccess,
"api"
},
RequireConsent = false,
AllowRememberConsent = true,
AccessTokenType = AccessTokenType.Reference,
RedirectUris = new List<string>
{
"http://localhost:48197/"
},
PostLogoutRedirectUris = new List<string>
{
"http://localhost:48197/"
},
AllowAccessTokensViaBrowser = true
}
MVC application project
Startup configuration
const string AuthorityUri = "https://localhost:44311/identity";
public void Configuration(IAppBuilder app)
{
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "mvc",
Authority = AuthorityUri,
RedirectUri = "http://localhost:48197/",
ResponseType = "code id_token",
Scope = "openid profile email roles api offline_access",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
},
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
var tokenClient = new TokenClient(AuthorityUri + "/connect/token", "mvc", "secret");
TokenResponse tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);
if (tokenResponse.IsError)
throw new Exception(tokenResponse.Error);
UserInfoClient userInfoClient = new UserInfoClient(AuthorityUri + "/connect/userinfo");
UserInfoResponse userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
ClaimsIdentity id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(userInfoResponse.Claims);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
},
RedirectToIdentityProvider = n => { // more code }
}
});
}
After I receive access token, I store it in the sessionStorage.
@model IEnumerable<System.Security.Claims.Claim>
<script>
sessionStorage.accessToken = '@Model.First(c => c.Type == "access_token").Value';
</script>
Following JavaScript function is used to perform ajax requests:
function ajaxRequest(requestType, url, parameters)
{
var headers = {};
if (sessionStorage.accessToken) {
headers['Authorization'] = 'Bearer ' + sessionStorage.accessToken;
}
$.ajax({
url: url,
method: requestType,
dataType: 'json',
data: parameters,
headers: headers
});
}
WebAPI project
Startup configuration:
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44311/identity",
ClientId = "mvc",
ClientSecret = "secret",
RequiredScopes = new[] { "api", "roles" }
});
Could you tell me what I'm doing wrong?
Edit (solved)
I had invalid configuration of WebAPI because nomenclature is misleading. It turned out that ClientId and ClientSecret should contian name of scope and its secret (link to reported issue).
Following Startup configuration of WebAPI works as intended:
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44311/identity",
// It has been changed:
ClientId = "api", // Scope name
ClientSecret = "secret", // Scope secret
RequiredScopes = new[] { "api", "roles" }
});