Using IdentityServer4, I am getting an unauthorized_client error after updating my code for .net core 3.
My client is set up as follows in IdentityServer:
new Client
{
ClientId = "testclient",
ClientName = "My Test Client",
RequireConsent = false,
AllowedGrantTypes = GrantTypes.Implicit,
ClientSecrets = { new Secret("secret".Sha256()) },
RedirectUris = { "https://localhost:50691/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:50691/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId, // identity resource
"testscope" // api resource
}
},
I am trying to retrieve an access token using the following code in my client application:
public async Task<TokenResponse> GetAccessTokenAsync(string IdentityServerBaseAddress, string IdentityServerClientId)
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync(IdentityServerBaseAddress);
if (disco.IsError)
{
Console.WriteLine($"Disco error: {disco.Error}");
return null;
}
Console.WriteLine($"Token endpoint: {disco.TokenEndpoint}");
Console.WriteLine();
// TODO: Get secret from Azure Key Vault
// request token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint, // "https://localhost:5000/connect/token"
ClientId = "testclient", // valid clientid
ClientSecret = "secret",
Scope = "testscope"
});
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return null;
}
return tokenResponse;
}
tokenResponse.IsError is returning true (hence the method returns null) and Error is set to "unauthorized_client". I am basing this code off the documentation at http://docs.identityserver.io/en/latest/quickstarts/1_client_credentials.html?highlight=requestclientcredentialstokenasync. My clientid is valid and IdentityServer is validating the user upon login for this client. I'm pretty sure the issue is something simple that I'm just not seeing? Any help will be much appreciated.
Since you're using a clientId and ClientSecret, so I think you need to change your Client config to use GrantTypes.ClientCredentials, not GrantTypes.Implicit.
Change:
AllowedGrantTypes = GrantTypes.Implicit,
To
AllowedGrantTypes = GrantTypes.ClientCredentials,
Related
I am building a .Net core 6 mvc website which will interact with an API built by an external party. Amongst other things, the user authentication is handled by the API. The API responds with a JWT bearer token once user is authenticated and I need to tie that in to my website to Authorize controller methods.
At this point I call the API and successfully receive the token as expected, however after days of struggling to get [Authorize] to work in the controllers with the token, I am completely lost and hoping for some guidance.
After scrapping multiple iterations of code, this what I currently have.... (excuse the mess)
public async Task<TokenResponse> LoginAsync(string email, string password)
{
var userLogin = new UserLogin
{
Email = email,
Password = password
};
string encoded = System.Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(email + ":" + password));
var client = new RestClient("api location");
var request = new RestRequest();
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Authorization", "Basic " + encoded);
var response = await client.GetAsync(request);
var result = JsonConvert.DeserializeObject<TokenResponse>(response.Content);
return result;
}
public async Task<IActionResult> LoginPostAsync(LoginViewModel viewModel)
{
var tokenResponse = await _userManagementService
.LoginAsync(viewModel.Email, viewModel.Password);
if (!string.IsNullOrEmpty(tokenResponse.access_token))
{
var handler = new JwtSecurityTokenHandler();
var jwtSecurityToken = handler.ReadJwtToken(tokenResponse.access_token);
var jti = jwtSecurityToken.Claims.First(claim => claim.Type == "jti").Value;
var account_type = jwtSecurityToken.Claims.First(claim => claim.Type == "account_type").Value;
var userId = jwtSecurityToken.Claims.First(claim => claim.Type == "user_id").Value;
var email = jwtSecurityToken.Claims.First(claim => claim.Type == "email").Value;
var iss = jwtSecurityToken.Claims.First(claim => claim.Type == "iss").Value;
string[] userRoles = { "admin", "candidate",};
HttpContext context = new DefaultHttpContext();
var accessToken = tokenResponse.access_token;
//var userClaims = new List<Claim>()
// {
// new Claim("email", email),
// new Claim("account_type", account_type),
// new Claim("jti", jti),
// };
//var userIdentity = new ClaimsIdentity(userClaims, "User Identity");
//var userPrincipal = new ClaimsPrincipal(new[] { userIdentity });
//context.SignInAsync(userPrincipal);
//Response.Cookies.Append(
// Constants.XAccessToken,
// tokenResponse.access_token, new CookieOptions
// {
// Expires = DateTimeOffset.UtcNow.AddMinutes(1),
// HttpOnly = true,
// SameSite = SameSiteMode.Strict
// });
//return new AuthenticateResponse(user, token);
SetJWTCookie(accessToken);
return RedirectToAction("index", "Home", new { area = "CandidateDashboard" });
}
return Unauthorized();
}
Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(config =>
{
config.SaveToken = true;
config.RequireHttpsMetadata = false;
config.TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = false,
ValidateIssuer = true,
ValidIssuer = "issue data",
ValidateIssuerSigningKey = false,
};
config.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["Bearer"];
return Task.CompletedTask;
}
};
});
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult Index()
{
return View();
}
This is the what I see in dev console.
--bearer error="invalid_token", error_description="the signature key was not found"
Payload from Bearer
{
"iss": "data here",
"exp": 1647323406,
"nbf": 1647319746,
"iat": 1647319806,
"jti": "e8f297d3-blah blah",
"account_type": "candidate",
"user_id": "2342342342",
"email": "email#email.com"
}
The core problem is that AddJwtBearer by default only trusts token issued by someone it trusts (the issuer) because it needs to verify the signature of the token. You of course want to verify it so a hacker doesn't send fake/forged tokens to your API.
So either you need to add that
.AddJwtBearer(opt =>
{
opt.Authority = "https://issuer.com"
In this way, AddJwtBearer will download the public signing key automatically for you.
Or you need to add the public signing key manually to AddJwtBearer.
see https://devblogs.microsoft.com/dotnet/jwt-validation-and-authorization-in-asp-net-core/
I'm trying to get IdentityServer4 to work but unfortunately no luck. I'll explain the issue in more detail. I'm using IdentityServer4 and also .NET core Identity. I have a .net core mvc application which has login page. You basically login with username and password. When you login I need to generate jwt token I'm doing this using the following code:
[HttpGet]
public async Task<IActionResult> GetClientToken(string clientId, string clientSecret, string grantType, string scope, string username, string password)
{
var serverClient = HttpClientFactory.CreateClient();
var discoveryDocument = await serverClient.GetDiscoveryDocumentAsync($"{Request.Scheme}://{Request.Host.Value}");
var tokenClient = HttpClientFactory.CreateClient();
var tokenResponse = await tokenClient.RequestPasswordTokenAsync(
new PasswordTokenRequest
{
ClientId = clientId,
ClientSecret = clientSecret,
GrantType = grantType,
Address = discoveryDocument.TokenEndpoint,
UserName = username,
Password = password,
Scope = scope,
});
if (!tokenResponse.IsError)
{
return Ok(new TokenResponseModel()
{
access_token = tokenResponse.AccessToken,
refresh_token = tokenResponse.RefreshToken,
expires_in = tokenResponse.ExpiresIn,
scope = tokenResponse.Scope,
token_type = tokenResponse.TokenType,
});
}
return BadRequest(tokenResponse.Error);
}
Every time I request for a token I get unauthorised client.
My seeding data is as follows:
public static IEnumerable<ApiResource> GetApis() =>
new List<ApiResource>
{
new ApiResource("AppointmentBookingApi"),
new ApiResource("PaymentApi", new string[] { "patient.portal.api.payment" }),
};
public static IEnumerable<IdentityResource> GetIdentityResources() =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResource
{
Name = "patient.portal.api",
UserClaims =
{
"patient.portal",
},
}
};
public static IEnumerable<Client> GetClients() =>
new List<Client>
{
new Client
{
ClientId = "patient.portal.client.refresh",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
RequirePkce = true,
RedirectUris = { "https://localhost:44307/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:44307/Home/Index" },
AllowedScopes =
{
"AppointmentBookingApi",
"PaymentApi",
IdentityServerConstants.StandardScopes.OpenId,
"patient.portal.api",
},
AllowOfflineAccess = true,
RequireConsent = false,
},
new Client
{
ClientId = "patient.portal.client.code",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes =
{
"AppointmentBookingApi",
},
},
};
does anyone know where I'm I going wrong here????
adopting from scottbrady91.com, I'm trying to have an Apple external authentication on our site. I've had Microsoft one working, but not the Apple one yet. The user is already directed to appleid.apple.com, but after authentication, it's returned to https://iluvrun.com/signin-apple (which is correct), but this isn't handled and so the user gets a 404 error.
To be honest I don't know how signin-facebook, signin-google or signin-oidc work, but they just do. So I have problems figuring out why signin-apple isn't being handled.
The site is built using ASP.NET Web Forms. Below is what I have at Startup.Auth.cs:
namespace ILR
{
public partial class Startup {
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions("Apple")
{
ClientId = "com.iluvrun.login",
Authority = "https://appleid.apple.com/auth/authorize",
SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
RedirectUri = "https://iluvrun.com/signin-apple",
PostLogoutRedirectUri = "https://iluvrun.com",
Scope = "name email",
ResponseType = OpenIdConnectResponseType.Code,
ResponseMode = OpenIdConnectResponseMode.FormPost,
CallbackPath = PathString.FromUriComponent("/signin-apple"),
Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = "https://appleid.apple.com/auth/authorize",
TokenEndpoint = "https://appleid.apple.com/auth/token"
},
TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "https://appleid.apple.com",
IssuerSigningKey = new JsonWebKeySet(GetKeysAsync().Result).Keys[0]
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = (context) =>
{
context.TokenEndpointRequest.ClientSecret = TokenGenerator.CreateNewToken();
return Task.CompletedTask;
},
AuthenticationFailed = (context) =>
{
context.HandleResponse();
context.Response.Redirect("/Account/Login?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
}
},
ProtocolValidator = new OpenIdConnectProtocolValidator
{
RequireNonce = false,
RequireStateValidation = false
}
}
);
}
private static async Task<string> GetKeysAsync()
{
string jwks = await new HttpClient().GetStringAsync("https://appleid.apple.com/auth/keys");
return jwks;
}
}
public static class TokenGenerator
{
public static string CreateNewToken()
{
const string iss = "CHM57Z5A6";
const string aud = "https://appleid.apple.com";
const string sub = "com.iluvrun.login";
const string privateKey = "XXXX"; // contents of .p8 file
CngKey cngKey = CngKey.Import(Convert.FromBase64String(privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
JwtSecurityToken token = handler.CreateJwtSecurityToken(
issuer: iss,
audience: aud,
subject: new ClaimsIdentity(new List<Claim> { new Claim("sub", sub) }),
expires: DateTime.UtcNow.AddMinutes(5),
issuedAt: DateTime.UtcNow,
notBefore: DateTime.UtcNow,
signingCredentials: new SigningCredentials(new ECDsaSecurityKey(new ECDsaCng(cngKey)), SecurityAlgorithms.EcdsaSha256));
return handler.WriteToken(token);
}
}
}
Does anyone have any clue what I miss to get this working?
You are on the right track and your question helped me to quick start my own solution for Apple ID OpenIdConnect OWIN integration in my project. After finding your post here it took me quite long to fix all issues.
After using your code I've got the same 404 error.
404 error
This was due to unhandled exception in CreateNewToken() method, which wasn't able to generate valid token. In my case it was missing Azure configuration for my AppService more described in CngKey.Import on azure:
WEBSITE_LOAD_USER_PROFILE = 1
After setting this configuration in Azure I moved to next issue:
Token endpoint wasn't called
This was due to missing configuration in OpenIdConnectAuthenticationOptions:
RedeemCode = true
This option trigger all the next processing of the authentication pipeline inside OpenIdConnect (TokenResponseReceived, SecurityTokenReceived, SecurityTokenValidated)
AccountController.ExternalLoginCallback token processing issue
This was tricky. Because all you get is after calling
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
is getting:
loginInfo == null
So after reading lot of issue articles on this topic like OWIN OpenID provider - GetExternalLoginInfo() returns null and tries, the only final workaround was to add https://github.com/Sustainsys/owin-cookie-saver to my Startup.cs, which fixed problem of missing token cookies. Its marked as legacy, but it was my only option to fix this.
So final OpenIdConnect options config for my working solution is:
var appleIdOptions = new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "https://appleid.apple.com",
ClientId = "[APPLE_CLIENT_ID_HERE]",
Authority = "https://appleid.apple.com/auth/authorize",
SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
RedirectUri = "https://www.europeanart.eu/signin-apple",
PostLogoutRedirectUri = "https://www.europeanart.eu",
Scope = OpenIdConnectScope.Email,
RedeemCode = true,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
ResponseMode = OpenIdConnectResponseMode.FormPost,
CallbackPath = PathString.FromUriComponent("/signin-apple"),
Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = "https://appleid.apple.com/auth/authorize",
TokenEndpoint = "https://appleid.apple.com/auth/token"
},
TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "https://appleid.apple.com",
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
IssuerSigningKeys = new JsonWebKeySet(GetKeys()).Keys,
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = (context) =>
{
var clientToken = JwtTokenGenerator.CreateNewToken();
logger.LogInfo("Apple: clientToken generated");
context.TokenEndpointRequest.ClientSecret = clientToken;
logger.LogInfo("Apple: TokenEndpointRequest ready");
return Task.FromResult(0);
},
TokenResponseReceived = (context) =>
{
logger.LogInfo("Apple: TokenResponseReceived");
return Task.FromResult(0);
},
SecurityTokenReceived = (context) =>
{
logger.LogInfo("Apple: SecurityTokenReceived");
return Task.FromResult(0);
},
SecurityTokenValidated = (context) =>
{
string userID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
logger.LogInfo("Apple: SecurityTokenValidated with userID=" + userID);
return Task.FromResult(0);
},
RedirectToIdentityProvider = (context) =>
{
logger.LogInfo("Apple: RedirectToIdentityProvider");
if(context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
logger.LogInfo("Apple: RedirectToIdentityProvider -> Authenticate()");
}
else if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Token)
{
logger.LogInfo("Apple: RedirectToIdentityProvider -> Token()");
}
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.HandleResponse();
logger.LogError("Apple Authentication Failed.", context.Exception);
context.Response.Redirect("/Account/Login?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
}
},
ProtocolValidator = new OpenIdConnectProtocolValidator
{
RequireNonce = false,
RequireStateValidation = false
}
};
I'm protecting a Web API with Identity Server 4.
If an external app tries to access it using client credentials but does not pass in the access token, I get, as expected, the Unauthorized response.
The problem here is that the response does not include the WWW-Authenticate header as I was expecting, as stated in the OAuth spec.
Am I missing some config in Identity Server? Or is it something wrong with the Identity Server implementation?
The relevant code parts follow:
Client registration on Identity Server:
new Client()
{
ClientId = "datalookup.clientcredentials",
ClientName = "Data Lookup Client with Client Credentials",
AlwaysIncludeUserClaimsInIdToken = true,
AlwaysSendClientClaims = true,
AllowOfflineAccess = false,
ClientSecrets =
{
new Secret("XXX".Sha256())
},
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes =
{
Scopes.DataLookup.Monitoring,
Scopes.DataLookup.VatNumber
},
ClientClaimsPrefix = "client-",
Claims =
{
new Claim("subs", "1000")
}
}
ApiResource registration on Identity Server:
new ApiResource()
{
Name = "datalookup",
DisplayName = "Data Lookup Web API",
ApiSecrets =
{
new Secret("XXX".Sha256())
},
UserClaims =
{
JwtClaimTypes.Name,
JwtClaimTypes.Email,
JwtClaimTypes.Profile,
"user-subs"
},
Scopes =
{
new Scope()
{
Name = Scopes.DataLookup.Monitoring,
DisplayName = "Access to the monitoring endpoints",
},
new Scope()
{
Name = Scopes.DataLookup.VatNumber,
DisplayName = "Access to the VAT Number lookup endpoints",
Required = true
}
}
}
Authentication configuration in the Web API:
public void ConfigureServices(IServiceCollection services)
{
(...)
services.AddMvc();
services
.AddAuthorization(
(options) =>
{
options.AddPolicy(
Policies.Monitoring,
(policy) =>
{
policy.RequireScope(Policies.Scopes.Monitoring);
});
options.AddPolicy(
Policies.VatNumber,
(policy) =>
{
policy.RequireScope(Policies.Scopes.VatNumber);
policy.RequireClientSubscription();
});
});
services.AddAuthorizationHandlers();
services
.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(
(options) =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "datalookup";
});
(...)
}
Client accessing the Web API:
using (HttpClient client = new HttpClient())
{
// client.SetBearerToken(accessToken);
using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Constants.WebApiEndpoint))
{
using (HttpResponseMessage response = await client.SendAsync(request).ConfigureAwait(false))
{
if (!response.IsSuccessStatusCode)
{
ConsoleHelper.WriteErrorLine(response);
return;
}
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
ConsoleHelper.WriteInformationLine(content);
}
}
}
Notice that client.SetBearerToken(accessToken) is commented, so that is why I was expecting the response to include the WWW-Authenticate header.
The whole idea behind this is to implement a feature on a client library to deal with the Http Bearer challenge (as, for example, the Azure KeyVault client library does).
After using Facebook access_token and userId to get Facebook information and use email to login/register Identity Server account, I want to get the access_token of Identity Server account to login the main server account.
It might be contained the sub (user id) however I cannot use the userManager's method to find the token.
Here is my sample code for getting access_token but both not work:
[HttpPost]
[AllowAnonymous]
public async Task<string> GetTokenForIOS(FacebookUserModel model)
{
model.email = await GetEmail(model);
var user = await _userManager.FindByNameAsync(model.email);
var userInfo = _userManager.Users.SingleOrDefault(x => x.Email == model.email);
if (user != null)
{
// sign in identityServer
await _signInManager.SignInAsync(user, isPersistent: false);
// the first way to get access_token
var externalAccessToken = await _userManager.GetAuthenticationTokenAsync(user, "Facebook", "access_token");
// the second way to get access_token
string token = await GetAccessToken();
//return access_token
return token;
}
return "";
}
public async Task<string> GetAccessToken()
{
// discover endpoints from metadata
var disco = await DiscoveryClient.GetAsync("https://localhost:44355");
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "client.ios");
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api");
if (tokenResponse.IsError)
{
return "";
}
else
{
Console.WriteLine(tokenResponse.Json);
JToken jObject = tokenResponse.Json;
return tokenResponse.Json.ToString();
}
}
Here is my client config:
new Client
{
ClientId = "client.ios",
RequireClientSecret = false,
ClientName = "iOS app client",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireConsent = false,
RedirectUris = { "net.openid.appauthdemo:/oauth2redirect" },
PostLogoutRedirectUris = { "net.openid.appauthdemo:/oauth2redirect" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.Phone,
"api"
},
AllowOfflineAccess = true
}
Is there any way can help me to find the user access_token for token response in this scenario?