WSO2 Identity Server with OpenId Connect - asp.net-mvc

I am trying to use WSO2 Identity Server (5.1.0) with Asp.Net MVC, as a proof of concept i created a sample asp.net MVC project in visual studio 2015.
Following the WSO2 Guide, i have configured the identity server as required.
https://docs.wso2.com/display/IS510/OpenID+Connect+with+the+WSO2+Identity+Server+and+WSO2+OAuth2+Playground
On the sample application, i have added reference to Microsoft.Owin.Security.OpenIdConnect and added code to ConfigureAuth in Startup.Auth.cs file.
public void ConfigureAuth(IAppBuilder app)
{
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
// Enables the application to remember the second login verification factor such as phone or email.
// Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
// This is similar to the RememberMe option when you log in.
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
// Uncomment the following lines to enable logging in with third party login providers
//app.UseMicrosoftAccountAuthentication(
// clientId: "",
// clientSecret: "");
//app.UseTwitterAuthentication(
// consumerKey: "",
// consumerSecret: "");
//app.UseFacebookAuthentication(
// appId: "",
// appSecret: "");
//app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
//{
// ClientId = "",
// ClientSecret = ""
//});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "SENmQQ9fOWcrqXjK1u3lXINhXtEa",
ClientSecret = "bFBJQqj4GT2Wfv8735fTTuHh3Isa",
Authority = "https://localhost:9443",
RedirectUri = "https://wso2openid.local.co.uk/Account/ExternalLoginCallback",
SignInAsAuthenticationType = "ClientCredCookie",
ResponseType = "id_token token",
Scope = "openid",
Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = "https://localhost:9443/oauth2/authorize",
TokenEndpoint = "https://localhost:9443/oauth2/token"
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = n =>
{
return Task.FromResult(0);
},
SecurityTokenReceived = n =>
{
return Task.FromResult(0);
},
AuthorizationCodeReceived = n =>
{
return Task.FromResult(0);
},
SecurityTokenValidated = n =>
{
var token = n.ProtocolMessage.AccessToken;
// persist access token in cookie
if (!string.IsNullOrEmpty(token))
{
n.AuthenticationTicket.Identity.AddClaim(
new Claim("access_token", token));
}
return Task.FromResult(0);
},
AuthenticationFailed = notification =>
{
if (string.Equals(notification.ProtocolMessage.Error, "access_denied", StringComparison.Ordinal))
{
notification.HandleResponse();
notification.Response.Redirect("/");
}
return Task.FromResult<object>(null);
}
}
});
}
When i run the application, on login it redirects to WSO2 Identity Server login and manage to login but when it redirect to Account\ExternalLoginCallback, the logininfo is always null.
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
Any advise as to why this is not working will be appreciated.?
NB: I tried to put a break point on SecurityTokenValidated but it did not hit that break point. The only breakpoint which i got hit was RedirectToIdentityProvider.

It's work well for me:
Step1 : Config WSO2 with this:
https://docs.wso2.com/display/IS570/Logging+in+to+a+.NET+application+using+the+Identity+Server
Step2:
public async Task<RedirectResult> LoginOAuth()
{
var url = "https://localhost:9443/oauth2/authorize?response_type=code&client_id=5a8urZQAc0r4R7iUS9ar1wOoq9Ma&scope=openid&redirect_uri=http://localhost:49545/Home/GetCode";
var client = new HttpClient();
var response = await client.GetAsync(url);
string urlDistance = response.RequestMessage.RequestUri.ToString();
client.Dispose();
return Redirect(urlDistance);
}
public async Task<RedirectToRouteResult> GetCode()
{
//باشد GetCode همشون حتما باید
var client = new HttpClient();
string code = Request.QueryString["code"];
string sessionState = Request.QueryString["session_state"];
string client_id = Request.QueryString["client_id"];
client.Dispose();
//از طریق ارسال کد میخواد توکن رو بگیره
//****************
var values = new Dictionary<string, string>
{
{ "code", code },
{ "sessionState", sessionState },
{ "client_id", "5a8urZQAc0r4R7iUS9ar1wOoq9Ma" },
{ "client_secret", "b0yefcCc4ftVYJm7ffQi2IZZ0eMa" },
{ "grant_type", "authorization_code" },
{ "redirect_uri", "http://localhost:49545/Home/GetCode" }//??????????????
};
var content = new FormUrlEncodedContent(values);
client = new HttpClient();
var response2 = await client.PostAsync("https://localhost:9443/oauth2/token", content);
string responseString = await response2.Content.ReadAsStringAsync();
JObject jsonResult = JObject.Parse(responseString);
string access_token = jsonResult["access_token"].ToString();
string refresh_token = jsonResult["refresh_token"].ToString();
string scope = jsonResult["scope"].ToString();
string id_token = jsonResult["id_token"].ToString();
string token_type = jsonResult["token_type"].ToString();
string expires_in = jsonResult["expires_in"].ToString();
//**************
var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("https://localhost:9443/oauth2/userinfo?schema=openid");
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", access_token);
string result = await httpClient.GetStringAsync("/oauth2/userinfo?schema=openid");
return RedirectToAction("Contact");
}

Related

Redirect back to the ASP.NET Mvc Client after Sign-out from IdentityServer

I want to redirect back to my client after sign-out from local, then the IS4; My AspNetCore Mvc client works correctly and redirect back to the client after sign-out, but the AspNet Mvc (not Core) it doesn't.
here is my Startup.Configuration method:
public void Configuration(IAppBuilder app)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
SignInAsAuthenticationType = "Cookies",
Authority = "https://localhost:5000",
UseTokenLifetime = false,
// RedeemCode = true,
ClientId = "aspNet_client",
ClientSecret = "secret",
RedirectUri = "https://localhost:44343/sigin-oidc",
PostLogoutRedirectUri = "https://localhost:44343/signout-callback-oidc",
SaveTokens = true,
ResponseType = "code id_token",
Scope = "openid profile offline_access",
TokenValidationParameters = new TokenValidationParameters()
{
NameClaimType = JwtClaimTypes.PreferredUserName,
RoleClaimType = JwtClaimTypes.Role,
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = onAuthenticationFailed,
MessageReceived = onMessageReceived,
// AuthorizationCodeReceived = onAuthorizationCodeReceived
}
});
}
I used this method to sign-out:
public ActionResult SignOut()
{
Request.GetOwinContext().Authentication.SignOut();
return Redirect("/");
}
I used this method too:
public ActionResult SignOut()
{
System.Web.HttpContext.Current.GetOwinContext().Authentication.SignOut(
new AuthenticationProperties
{
RedirectUri = "https://localhost:44343"
},
CookieAuthenticationDefaults.AuthenticationType,
OpenIdConnectAuthenticationDefaults.AuthenticationType
);
//"Cookies", "OpenIdConnect"
}
But not worked. So my question is:
How to automatic redirect back to my AspNetMvc Client after sign-out?
This was an error reported long time ago on IdentityServer3. It got fixed here by setting IdTokenHint on logout. In this case as we use IdentityServer4, we can implement similar fix manually on ASP.NET MVC app. Here is changes need to make:
on IdentityServer project set PostLogoutRedirectUris for the client:
new Client
{
ClientId = "aspNet_client",
//All other settings ...
PostLogoutRedirectUris = { "http://localhost:44343" },
},
On ASP.NET mvc application, set OpenIdConnectAuthenticationOptions - PostLogoutRedirectUri to the same value as step 1
Change Notifications - SecurityTokenValidated and RedirectToIdentityProvider to set IdTokenHint on logout
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
// other settings...
PostLogoutRedirectUri = "http://localhost:44343",
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
n.AuthenticationTicket.Identity.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
return Task.FromResult(0);
},
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var id_token_claim = n.OwinContext.Authentication.User.Claims.FirstOrDefault(x => x.Type == "id_token");
if (id_token_claim != null)
{
n.ProtocolMessage.IdTokenHint = id_token_claim.Value;
}
}
return Task.FromResult(0);
}
}
});
If you want to redirect automatically set AccountOptions - AutomaticRedirectAfterSignOut to true on IdentityServer, default value is false.
Implemented it myself here

MSIS9649: Received invalid OAuth request. The 'assertion' parameter value is not a valid access token

I am trying to implement ADFS4 - OAuth (OpenID connect) for authentication and webapp to webapi communication.
I have configured ADFS application group accordingly and use OpenIdconnectauth pipeline in webapp for authentication. In order to call webapi, if I request accesstoken using just client credential grant, it works fine as I receive the valid access token and able to get to the api. However, the access token does not have any user details in it which I need it from the webapi end.
So, then I tried by creating UserAssertion object from bootstrapcontext.token. But this time, when ever I request access token, I receive this error as mentioned in the title.
Here is the code snippet:
AuthenticationContext authContext = null;
AuthenticationResult result = null;
authContext = new AuthenticationContext(Startup.authority, false);
ClientCredential credential = new ClientCredential(Startup.clientId, Startup.appKey);
string usercheck = User.Identity.Name; //For checking, returns username
var bootstrapContext = ClaimsPrincipal.Current.Identities.First().BootstrapContext as System.IdentityModel.Tokens.BootstrapContext;
string username = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null ? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value : ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;
string userAccessToken = bootstrapContext.Token;
UserAssertion userAssertion = new UserAssertion(bootstrapContext.Token, "urn:ietf:params:oauth:grant-type:jwt-bearer", username);
string accessToken = null;
HttpClient httpClient = new HttpClient();
try {
//result = authContext.AcquireTokenAsync(Startup.apiResourceId, credential).Result; // This works fine but no user details in the token
result = authContext.AcquireTokenAsync(Startup.apiResourceId, credential, userAssertion).Result;
}
Here is how the Startup.ConfigureAuth(IAppBuilder app) looks like in both webapp and webapi:
In webapp:
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
MetadataAddress = metadataAddress,
PostLogoutRedirectUri = postLogoutRedirectUri,
RedirectUri = postLogoutRedirectUri,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters()
{
SaveSigninToken = true
},
ResponseType = "code id_token",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Redirect("/Error?message=" + context.Exception.Message);
return Task.FromResult(0);
}
}
});
}
And in webapi:
public void ConfigureAuth(IAppBuilder app)
{
JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
app.UseActiveDirectoryFederationServicesBearerAuthentication(
new ActiveDirectoryFederationServicesBearerAuthenticationOptions
{
MetadataEndpoint = ConfigurationManager.AppSettings["ida:AdfsMetadataEndpoint"],
TokenValidationParameters = new TokenValidationParameters() {
SaveSigninToken = true,
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
}
});
}
I reckon that the token that I am passing in to the userassertion is incorrect. But how can I fix this? Is there any other way which I can get the user details in to the access token. I really appreciate if anyone can help us to solve this issue?
Thanks.
You have to use authorization code flow to get the MVC app to talk to the API. Vittorio has a nice post on it here, although it talks about azure.
In order to do that you need to handle the AuthorizationCodeReceived Event via Notifications on the OpenIdConnectAuthenticationOptions from Startup.ConfigureAuth(IAppBuilder app)
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions {
...
Notifications = new OpenIdConnectAuthenticationNotifications {
AuthorizationCodeReceived = async code => {
ClientCredential credential = new ClientCredential(Startup.clientId, Startup.appKey);
AuthenticationContext authContext = new AuthenticationContext(Startup.authority, false);
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(
code.Code,
new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)),
credential,
Startup.apiResourceId);
}
}
When you are ready to make the call you acquire your token silently.
var authContext = new AuthenticationContext(Startup.authority, false);
var credential = new ClientCredential(Startup.clientId, Startup.appKey);
var claim = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
var userId = new UserIdentifier(claim, UserIdentifierType.UniqueId);
result = await authContext.AcquireTokenSilentAsync(
Startup.apiResourceId,
credential,
userId);
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer",
result.AccessToken);

IdentityServer3: OWIN Katana middleware is throwing "invalid_client" error as it cannot get a token

We are using IdentityServer3 as the identity provider and OWIN Katana middleware to do the handshake based on OpenId Connect. The authentication works fine as we were redirected to identity server and back to the originating website. But the issue of invalid_client appears when I try to retrieve the tokens and get claims in the "OpenIdConnectAuthenticationNotifications".
Please check the code (startup class) below and the attached screenshot.
public sealed class Startup
{
public void Configuration(IAppBuilder app)
{
string ClientUri = #"https://client.local";
string IdServBaseUri = #"https://idm.website.com/core";l
string TokenEndpoint = #"https://idm.website.com/core/connect/token";
string UserInfoEndpoint = #"https://idm.website.com/core/connect/userinfo";
string ClientId = #"WebPortalDemo";
string ClientSecret = #"aG90apW2+DbX1wVnwwLD+eu17g3vPRIg7p1OnzT14TE=";
//AntiForgeryConfig.UniqueClaimTypeIdentifier = "sub";
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = ClientId,
Authority = IdServBaseUri,
RedirectUri = ClientUri,
PostLogoutRedirectUri = ClientUri,
ResponseType = "code id_token token",
Scope = "openid profile roles",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
},
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// use the code to get the access and refresh token
var tokenClient = new TokenClient(
TokenEndpoint,
ClientId,
ClientSecret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(UserInfoEndpoint);
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
//id.AddClaims(userInfoResponse.GetClaimsIdentity().Claims);
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 =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
});
}
}
The client configuration at the IdSvr3 has been specified to use Hybrid Flow and I have checked that the client Id and client secret many times to verify that they are correct.
Here is the client configuration at the server side:
I was able to resolve the issue by looking at the logs generated by identity server. The logs said the client secret is incorrect, when I have checked several times that the secret was exact to what was showing on the identity server. But then I realised that the secret should be the actual text and NOT the hashed one. The modified code that worked is below:
string ClientId = #"WebPortalDemo";
//string ClientSecret = #"aG90apW2+DbX1wVnwwLD+eu17g3vPRIg7p1OnzT14TE="; // Incorrect secret, didn't work
string ClientSecret = #"love"; // Actual text entered as secret, worked
Credit: #rawel

IdentityServer3 using LinkedIn as external login

Trying to use LinkedIn as an external login but I keep having this error. After authorization screen. It goes to /callback instead of the initial application.
There is an error determining which application you are signing into.
Return to the application and try again.
Here's my configuration
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseLinkedInAuthentication(new LinkedInAuthenticationOptions()
{
ClientId = "[...]",
ClientSecret = "[...]"
});
}
I'm not really sure how to configure the client either. I know we need the flow.Authorization code. But beyond that I'm lost.
new Client
{
ClientId = "[...]",
ClientName = "Linkedin Client",
Enabled = true,
Flow = Flows.AuthorizationCode,
RedirectUris =
new List<string> {_baseUrl, "http://localhost/mtthelloworld"},
PostLogoutRedirectUris =
new List<string> {_baseUrl},
ClientSecrets = new List<Secret>()
{
new Secret("[???]".Sha256())
},
}
Edit added ConfigureIdentityServerConfig:
private void ConfigureIdentityServer(IAppBuilder appBuilder)
{
var idsFactory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get());
idsFactory.UserService = new Registration<IUserService>(typeof(UserService));
var idsOptions = new IdentityServerOptions()
{
SiteName = "SSO",
SigningCertificate = VcCert.Load("CN=cert.local"),
Factory = idsFactory,
RequireSsl = false,
LoggingOptions = new LoggingOptions()
{
EnableWebApiDiagnostics = true,
EnableHttpLogging = true,
EnableKatanaLogging = true,
WebApiDiagnosticsIsVerbose = true
},
PluginConfiguration = ConfigureWsFederation,
AuthenticationOptions = new AuthenticationOptions()
{
IdentityProviders = ConfigureIdentityProviders
}
};
appBuilder.Map("/identity", idApp => { idApp.UseIdentityServer(idsOptions); });
appBuilder.UseIdentityServer(idsOptions);
}
First of all you have to obtain client id and client secret from LinkedIn. This is usually done by registering your application on the external provider side - LinkedIn.
Note that, when you are delegating authentication to external provider, then your server is a client and LinkedIn is a server.
Regarding the configuration method for external providers, do not call CookieAuthenticationMiddleware inside this method. Instead configure only the LinkedIn authentication middleware.
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseLinkedInAuthentication(new LinkedInAuthenticationOptions()
{
AuthenticationType = "LinkedIn",
SignInAsAuthenticationType = signInAsType,
ClientId = "[...]", // client id you've obtained from LinkedIn
ClientSecret = "[...]", // client secret you've obtained from LinkedIn
});
}
You don't need to create new Client as you're doing it in your second code snippet. These classes are for meant for clients of your application, and as I've said earlier in this case your application is the client.
EDIT
You do not need to configure CookieAuthenticationMiddleware in the server where you host IdentityServer3 - let's call this server an identity provider.
public void Configuration(IAppBuilder app)
{
app.UseIdentityServer(new IdentityServerOptions
{
// ...
AuthenticationOptions = new AuthenticationOptions
{
IdentityProviders = (IAppBuilder builder, string signInAsType) =>
{
builder.UseLinkedInAuthentication(new LinkedInAuthenticationOptions
{
AuthenticationType = "LinkedIn",
SignInAsAuthenticationType = signInAsType,
ClientId = "[...]",
ClientSecret = "[...]"
};
}
}
});
}
The question that you could ask is - well, then where do I need to use it? The answer is simple - use it in clients that are delegating authorization to your identity provider.
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
SignInAsAuthenticationType = "Cookies",
// ...
});
}

Connecting with Javascript to Web API protected by IdentityServer3

I have a Asp.NET MVC / WebAPI project with an embedded IdentityServer3.
I want both MVC and WebAPI to be protected by the IdentityServer. So I have used Authorize attribute on both MVC controllers and API controllers.
When surfing to my test page (which is protected) I get redirected to the IdentityServer login page. I enter my username and password and get authenticated and redirected back.
On the page I have a button that triggers a GET from javascript, with my access token in the authorization header, to my protected API. But here it fails with a 401 Unauthorized.
I get the access token to javascript by rendering it to the page with Razor.
I have 1 client in the IdentityServer set to hybrid flow. MVC uses cookies, while the API uses bearer tokens.
On my API HttpConfiguration I have set SuppressDefaultHostAuthentication. If I remove that line everything works, but then it uses cookies for the API which I don't want.
I use only HTTP and RequireSsl=false for now to avoid potential certificate problems.
I have tried for days to get this to work but I'm getting nowhere.
I don't even know how to debug this to get to cause of the 401.
By now I recognize just about every page that google suggests when I search for help.
Any ideas what it could be or how to debug this?
Here is my Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Trace()
.CreateLogger();
// MVC client
string authBaseAddress = "http://localhost:50319/identity";
string tokenEndpoint = authBaseAddress + "/connect/token";
string userInfoEndpoint = authBaseAddress + "/connect/userinfo";
string redirectUri = "http://localhost:50319/";
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "hybrid_clients",
Authority = authBaseAddress,
RedirectUri = redirectUri,
ResponseType = "code id_token token",
Scope = "openid profile roles sampleApi offline_access",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
},
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// use the code to get the access and refresh token
var tokenClient = new TokenClient(
tokenEndpoint,
"hybrid_clients",
"secret");
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(
new Uri(userInfoEndpoint),
tokenResponse.AccessToken);
var userInfoResponse = await userInfoClient.GetAsync();
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(userInfoResponse.GetClaimsIdentity().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);
},
// Attach the id_token for the logout roundtrip to IdentityServer
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
});
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
// web api
app.Map("/api", a =>
{
var config = new HttpConfiguration();
a.UseCors(CorsOptions.AllowAll);
a.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
//AuthenticationMode = AuthenticationMode.Active,
Authority = authBaseAddress,
RequiredScopes = new[] { "sampleApi" },
DelayLoadMetadata = true
});
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
config.MapHttpAttributeRoutes();
a.UseWebApi(config);
});
// Identity server
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseCors(CorsOptions.AllowAll);
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = new IdentityServerServiceFactory()
.UseInMemoryUsers(Users.Get())
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get()),
AuthenticationOptions = new IdentityServer3.Core.Configuration.AuthenticationOptions()
{
EnablePostSignOutAutoRedirect = true // Automatically redirects back to the client on signout
},
RequireSsl = false,
});
});
}
X509Certificate2 LoadCertificate()
{
return new X509Certificate2(
string.Format(#"{0}\bin\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
}
}
My client
new Client
{
Enabled = true,
ClientName = "Hybrid Clients",
ClientId = "hybrid_clients",
Flow = Flows.Hybrid,
//AllowAccessTokensViaBrowser = false,
RedirectUris = new List<string>
{
"http://localhost:50319/"
},
PostLogoutRedirectUris = new List<string>
{
"http://localhost:50319/"
},
AllowedScopes = new List<string>
{
"openid",
"profile",
"email",
"roles",
"address",
"all_claims",
"sampleApi",
"offline_access"
},
ClientSecrets = new List<Secret>
{
new Secret("secret".Sha256())
},
AccessTokenType = AccessTokenType.Reference,
LogoutSessionRequired = true
},
My scopes
public static class Scopes
{
public static IEnumerable<Scope> Get()
{
var scopes = new List<Scope>
{
StandardScopes.OpenId,
StandardScopes.Profile,
StandardScopes.Email,
StandardScopes.Address,
StandardScopes.OfflineAccess,
StandardScopes.RolesAlwaysInclude,
StandardScopes.AllClaims,
new Scope
{
Enabled = true,
Name = "roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
},
new Scope
{
Enabled = true,
DisplayName = "Sample API",
Name = "sampleApi",
Description = "Access to a sample API",
Type = ScopeType.Resource
}
};
return scopes;
}
}
My API
[Authorize]
public class SecuredApiController : ApiController
{
public IHttpActionResult Get()
{
var user = User as ClaimsPrincipal;
var claims = from c in user.Claims
select new
{
type = c.Type,
value = c.Value
};
return Json(claims);
}
}
Part of my Razor view
<button data-bind="click:callApi">Call API</button>
<span data-bind="text:apiResult"></span>
<script>
$(function() {
ko.myViewModel = new ClientAppViewModel('#ViewData["access_token"]');
ko.applyBindings(ko.myViewModel);
});
</script>
My JavaScript (KnockoutJS) that calls SecuredApi
function ClientAppViewModel(accessToken) {
var self = this;
self.accessToken = accessToken;
self.apiResult = ko.observable('empty');
self.callApi = function () {
console.log('CallApi');
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:50319/api/SecuredApi");
xhr.onload = function () {
self.apiResult(JSON.stringify(JSON.parse(xhr.response), null, 2));
};
xhr.setRequestHeader("Authorization", "Bearer " + self.accessToken);
xhr.send();
}
}
The Katana logs for the API are what you want. With these you will see why the API is returning a 401.
You can access these using the Microsoft.Owin log source (see Katana Documentation)

Resources