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" }
});
You need to use IdentityServer3's "Hybrid Flow".
Here's a tutorial on how to implement it with IdentityServer3. https://identityserver.github.io/Documentation/docsv2/overview/mvcGettingStarted.html
This page for an explanation of how the Hybrid Flow works, and how to implement it (using IdentityServer4 - which unlike IdentityServer3, is still actively developed in case you have the option to upgrade). http://docs.identityserver.io/en/release/quickstarts/5_hybrid_and_api_access.html.
Related
I am having an issue with Claims not populating with ClaimsPrinciple after creating a JWT. I am using ASP.NET Core 6 on VS 2022. The issue raised after configuring identity to include Roles and RolesUsers. I had no issues prior to including these 2 identity tables from the automated generated ones from IdentityModel.
now on creation, I show no errors and receive the JWT token without any issues, but afterwards when I try to authorize the user that log in the ClaimIdentity does not propagate and errors on _userManager.FindByEmailAsync(User.FindFirstValue(ClaimTypes.Email)) showing null.
Here is some code to show the current state of the project.
First is the Method that handles the validation for login users.
[Authorize]
[HttpGet]
public async Task<ActionResult<UserDto>> GetCurrentUser()
{
// Null Exception Error
var user = await _userManager.FindByEmailAsync(User.FindFirstValue(ClaimTypes.Email));
return CreateUserObject(user);
}
UserDto CreateUserObject( AppUser user )
{
return new UserDto
{
DisplayName = user.DisplayName,
Image = null,
Token = _tokenService.CreateToken(user),
Username = user.UserName
};
}
This is my Token Service that handles creating the JWT token from users that Register or Login.
public class TokenService
{
private readonly IConfiguration _config;
public TokenService(IConfiguration config)
{
_config = config;
}
public string CreateToken(AppUser user)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Email, user.Email)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["TokenKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddDays(7.0),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
This is what I changed prior to having this issue in my IdentityServiceExtension Class.
public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config)
{
services.AddIdentity<AppUser, AppRole>(opt => //Changed AddIdentityCore to AddIdentity to apply AppUser & AppRole
{
opt.Password.RequireNonAlphanumeric = false;
})
.AddEntityFrameworkStores<DataContext>()
.AddSignInManager<SignInManager<AppUser>>()
.AddRoleManager<RoleManager<AppRole>>(); //Added Role Manager for Roles to loaded.
var Key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"]));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = Key,
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddScoped<TokenService>();
// Added Roles to Policy
services.AddAuthorization(opt =>
{
opt.AddPolicy("Verified", pol =>
pol.RequireRole("User", "Staff", "Admin", "Guest"));
opt.AddPolicy("Restricted", pol =>
pol.RequireRole("User", "Staff", "Admin"));
opt.AddPolicy("EmployeeAccess", pol =>
pol.RequireRole("Staff", "Admin"));
opt.AddPolicy("ManagerAccess", pol =>
pol.RequireRole("Admin"));
});
//////////////////////
return services;
}
Hopefully this is enough information to help me with this issue. I have searched all over online and the resolutions I have seen does not match to my particular issue to solve the problem.
I surprisingly found the issue, so the reason I was having errors was due to not configuring Identity to handle all Identity Models. Prior to my change, I only handled users, but by adding roles and roleusers I had to handle all of Identity Model to prevent losing the claims. Due to this fact, I had to install another Microsoft Package,Microsoft.AspNetCore.Identity.UI, to gain access to the Identity Helper Method (.AddDefaultIdentity()) to configure the generated identity tables. Once added, Identity was fully configured and the issue was resolved. I hope anyone else that need help can use this as a possible solution.
I literally read the internet through, tried multiple approaches and so on - with no luck.
I am trying to make a pretty small WebApi project with a few controllers protected by a jwt token.
I generate the token using following code:
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("may the force"));
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims.ToArray()),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
I have setup the authentication (in Startup.cs) bits like
services.AddAuthentication(x => {
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}
)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("may the force"))
};
});
Using my AuthController I can generate the Token(first code block in this question) - and the Controllers needed to be "protected" is annotated with [Authorize]
If I take the result from AuthController and paste it at https://jwt.io the signature is valid.
I am testing the controllers using postman and setting the request header Authorization to Bearer <the token>
Can anyone point me in the right direction?
Thanks alot!
UPDATE: I misused the jwt.io site initially, thus some of the comments underneath here are no longer valid.
UPDATE 2: Realized that the server tried to use cookie based authentication. Changing the annotation to [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] made the trick - but still not sure why
I have web api project (ASP.Net 4.7) where i am Jwt Bearer Token for authentication and i would like to hook to an event when token is successfully validated call user information endpoint and add custom logic and add claims to identity.
I know we can do something similar with dotnet core https://www.jerriepelser.com/blog/aspnetcore-jwt-saving-bearer-token-as-claim/
Asp.Net 4.x
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(issuer + "/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever(), new HttpDocumentRetriever());
var discoveryDocument = Task.Run(() => configurationManager.GetConfigurationAsync()).GetAwaiter().GetResult();
app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = "Any",
ValidIssuer = issuer,
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) =>
{
return discoveryDocument.SigningKeys;
}
},
});
Please advise ?
Good morning,
I need to have in same project both web api and web app mvc.
Web api has to be protected via bearer token and web app mvc has to be authenticated via identity server.
Is it possible protecting a scope and a client in same project?
I think I have to do something like this in startup
//this to protect scope api1
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "http://localhost:5000/";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
});
//this to authenticate mvc client
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options =>
{
options.AccessDeniedPath = "/account/denied";
})
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000",
options.RequireHttpsMetadata = false;
options.ResponseType = "id_token token";
options.ClientId = "mvc-implicit";
options.SaveTokens = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("api1");
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapJsonKey("role", "role", "role");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
Now, I have to call my Api1 using client_credential with an external client.
But it returns me at login page.
Is it possible to do what I want?
Protected WebApi and Authenticated MVC client in same project?
Now, I have to call my Api1 using client_credential with an external client. But it returns me at login page.
That seems you misunderstand the scenario . Your MVC application is client also is a resource application which protected by Identity Server (in Config.cs):
public static IEnumerable<ApiResource> GetApis()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
};
}
I assume you have api controller in your MVC application :
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET: api/Values
[HttpGet]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
And you have config to protect the api actions by using AddJwtBearer :
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "http://localhost:5000/";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
});
That means any request to access the Get action should have an authentication bearer header with access token append , the access token is issued by your Identity Server(endpoint is http://localhost:5000/) and the audience is api1 .
Now your another client could use client credential flow to acquire access token to access your web application :
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
// request token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "client",
ClientSecret = "secret",
Scope = "api1"
});
And call your protected actions :
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = await apiClient.GetAsync("http://localhost:64146/api/values");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
So it won't redirect to login page , since client credential in fact is sending HTTP POST request to get access token with app's credential . There is no login page in this scenario .
I have a asp.net solution which consists of
1). asp.net identity server rc 3
2). asp.net Core web api
3). asp.net webform ( not in asp.net core, client)
I don't see any sample with identity server 4 and web form client. Can you please suggest how to authenticate web form user using identity server with asp.net identity and then call api with the access token ?
I don't see identity server 4 sample with web form client or sample
identity server 3 has a sample but it is doing everything in startup
When i see mvc client for identity server 4, it has all settings in configure method and then calls it like this
How will i apply Authorize attribute in webform so that i am redirected to identity server 4 for login and then after login when i call api like this:
how to change client for webform ?
new Client()
{
ClientId = "mvcClient",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets = new List<Secret>()
{
new Secret("secret".Sha256())
},
RequireConsent = false;
// where to redirect to after login
RedirectUris = { "http://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:5002" },
AllowedScopes =
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
StandardScopes.OfflineAccess.Name,
StandardScopes.Roles.Name,
"API"
}
}
new InMemoryUser()
{
Subject = "1",
Username = "testuser",
Password = "password",
Claims = new List<Claim>()
{
new Claim("name", "Alice"),
new Claim("Website", "http://alice.com"),
new Claim(JwtClaimTypes.Role, "admin")
}
}
return new List<Scope>()
{
StandardScopes.OpenId, // subject id
StandardScopes.Profile, // first name, last name
StandardScopes.OfflineAccess,
StandardScopes.Roles,
new Scope()
{
Name = "API",
Description = "API desc",
Type = ScopeType.Resource,
Emphasize = true,
IncludeAllClaimsForUser = true,
Claims = new List<ScopeClaim>
{
new ScopeClaim(ClaimTypes.Name),
new ScopeClaim(ClaimTypes.Role)
}
}
};
public void CallApiUsingClientCredentials()
{
var tokenClient = new TokenClient("http://localhost:5000/connect/token", "mvc", "secret");
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");
var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);
var content = await client.GetStringAsync("http://localhost:5001/identity");
var result = JArray.Parse(content).ToString();
}
[Authorize(Roles="admin)]
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
Late answer, but hopefully it helps someone, still supporting web forms.
There is no problem to use startup together with web forms. The only limitation is no place for AuthorizeAttribute there, but it's still not a problem, just put:
app.UseStageMarker(PipelineStage.Authenticate);
at the bottom of your
public void Configuration(IAppBuilder app)
method within OWIN Startup.An example Startup implementation could be fetched from my github. It works with MVC, Web Forms and additionally brings JWT validation from IdentityServer v.3' codebase, upgraded to compile with the latest OWIN libraries.
If I still left anything unclear, don't hesitate to ask in the comments.