IdentityServer4 with Google Authentication and Postman - oauth-2.0

I'm using IdentityServer4 with IdentityServer4.UI with authorization_code and Google authentication.
This is my Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// ...
const string connectionString = #"Data Source=.\SQLEXPRESS;database=Is4.LearnIs4;trusted_connection=yes;";
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services
.AddIdentityServer()
.AddDeveloperSigningCredential()
//.AddProfileService<IdentityServerProfileService>()
//.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
{
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
};
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
});
// Add jwt validation.
services
.AddAuthentication()
.AddGoogle("Google", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = "<my-client-id>";
options.ClientSecret = "<my-secret>";
options.AccessType = "offline";
})
.AddOpenIdConnect("demoidsrv", "IdentityServer", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "http://localhost:57547";
options.ClientId = "implicit";
options.ResponseType = "id_token";
options.SaveTokens = true;
options.CallbackPath = new PathString("/signin-idsrv");
options.SignedOutCallbackPath = new PathString("/signout-callback-idsrv");
options.RemoteSignOutPath = new PathString("/signout-idsrv");
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
// ...
}
And these are my client configurations:
/// <summary>
/// Seed database.
/// </summary>
/// <param name="app"></param>
public static void Seed(IApplicationBuilder app)
{
using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();
var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
context.Database.Migrate();
if (!context.Clients.Any())
{
foreach (var client in LoadClients())
context.Clients.Add(client.ToEntity());
context.SaveChanges();
}
if (!context.IdentityResources.Any())
{
foreach (var resource in LoadIdentityResources())
context.IdentityResources.Add(resource.ToEntity());
context.SaveChanges();
}
if (!context.ApiResources.Any())
{
foreach (var resource in LoadApiResources())
context.ApiResources.Add(resource.ToEntity());
context.SaveChanges();
}
}
}
/// <summary>
/// Load pre-defined identity resource.
/// </summary>
/// <returns></returns>
private static IEnumerable<IdentityResource> LoadIdentityResources()
{
var identityResources = new List<IdentityResource>();
identityResources.Add(new IdentityResources.OpenId());
identityResources.Add(new IdentityResources.Profile());
return identityResources;
}
/// <summary>
/// Load pre-defined api resources.
/// </summary>
/// <returns></returns>
private static IEnumerable<ApiResource> LoadApiResources()
{
var apiResources = new List<ApiResource>();
var api1Resource = new ApiResource("api1", "My API");
api1Resource.ApiSecrets = new List<Secret>();
api1Resource.ApiSecrets.Add(new Secret("secret".Sha256()));
apiResources.Add(api1Resource);
return apiResources;
}
/// <summary>
/// Load pre-defined clients.
/// </summary>
/// <returns></returns>
private static IEnumerable<Client> LoadClients()
{
var clients = new List<Client>();
var clientCredentialClient = new Client();
clientCredentialClient.ClientId = "client";
clientCredentialClient.AllowedGrantTypes = GrantTypes.ClientCredentials;
clientCredentialClient.ClientSecrets = new List<Secret>();
clientCredentialClient.ClientSecrets.Add(new Secret("secret".Sha256()));
clientCredentialClient.AllowedScopes = new List<string>();
clientCredentialClient.AllowedScopes.Add("api1");
clients.Add(clientCredentialClient);
var resourceOwnerPasswordClient = new Client();
resourceOwnerPasswordClient.ClientId = "ro.client";
resourceOwnerPasswordClient.AllowedGrantTypes = new List<string>();
resourceOwnerPasswordClient.AllowedGrantTypes.Add(GrantType.ResourceOwnerPassword);
resourceOwnerPasswordClient.AllowedGrantTypes.Add("refresh_token");
resourceOwnerPasswordClient.ClientSecrets = new List<Secret>();
resourceOwnerPasswordClient.ClientSecrets.Add(new Secret("secret".Sha256()));
resourceOwnerPasswordClient.AllowedScopes = new List<string>();
resourceOwnerPasswordClient.AllowedScopes.Add("api1");
resourceOwnerPasswordClient.AllowOfflineAccess = true;
resourceOwnerPasswordClient.RefreshTokenExpiration = TokenExpiration.Sliding;
resourceOwnerPasswordClient.RefreshTokenUsage = TokenUsage.ReUse;
resourceOwnerPasswordClient.SlidingRefreshTokenLifetime = 3600;
clients.Add(resourceOwnerPasswordClient);
var authorizationCodeClient = new Client();
authorizationCodeClient.ClientId = "mvc";
authorizationCodeClient.ClientName = "MVC Client";
authorizationCodeClient.AllowedGrantTypes = new List<string>();
authorizationCodeClient.AllowedGrantTypes.Add(GrantType.AuthorizationCode);
authorizationCodeClient.ClientSecrets = new List<Secret>();
authorizationCodeClient.ClientSecrets.Add(new Secret("secret".Sha256()));
authorizationCodeClient.RedirectUris = new List<string>();
authorizationCodeClient.RedirectUris.Add("http://localhost:4300");
authorizationCodeClient.PostLogoutRedirectUris = new List<string>();
authorizationCodeClient.PostLogoutRedirectUris.Add("http://localhost:5002/signout-callback-oidc");
authorizationCodeClient.RequirePkce = false;
authorizationCodeClient.AllowedScopes = new List<string>();
authorizationCodeClient.AllowedScopes.Add(IdentityServerConstants.StandardScopes.OpenId);
authorizationCodeClient.AllowedScopes.Add(IdentityServerConstants.StandardScopes.Profile);
authorizationCodeClient.AllowedScopes.Add("api1");
authorizationCodeClient.AllowOfflineAccess = true;
authorizationCodeClient.RequireConsent = false;
clients.Add(authorizationCodeClient);
var codeClient = new Client();
codeClient.ClientId = "js";
codeClient.ClientName = "JavaScript Client";
codeClient.AllowedGrantTypes = GrantTypes.Code;
codeClient.RequirePkce = true;
codeClient.RequireClientSecret = false;
codeClient.RedirectUris = new List<string>();
codeClient.RedirectUris.Add("http://localhost:5003/callback.html");
codeClient.PostLogoutRedirectUris = new List<string>();
codeClient.PostLogoutRedirectUris.Add("http://localhost:5003/index.html");
codeClient.AllowedCorsOrigins = new List<string>();
codeClient.AllowedCorsOrigins.Add("http://localhost:5003");
codeClient.AllowedScopes = new List<string>();
codeClient.AllowedScopes.Add(IdentityServerConstants.StandardScopes.OpenId);
codeClient.AllowedScopes.Add(IdentityServerConstants.StandardScopes.Profile);
codeClient.AllowedScopes.Add("api1");
clients.Add(codeClient);
return clients;
}
I made a request to IdentityServer4 as image attached below:
There is one popup displayed:
If I login using username and password, I can get access_token.
But when I clicked on Google button in Account/Login, I cannot get any access token.
This is what I got in Visual Studio ouput:
IdentityServer4.EntityFramework.Stores.PersistedGrantStore:Debug: P+z5g2/MhMzrbCEGMZOgx06VKeB38F1yx/y3C08vZlo= found in database: False
IdentityServer4.Stores.DefaultAuthorizationCodeStore:Debug: authorization_code grant with value: 4/KQGa0Oj2BztGi86v5SbFD0oVvnXIV8dZWFAQS16O-tyFVOcZXWvRQbByFb18Hhco5SgmkAQtPvk1ZD8lY8eLN9Q not found in store.
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 338.1411ms 500 text/html; charset=utf-8
IdentityServer4.Validation.TokenRequestValidator:Error: Invalid authorization code{ code = 4/KQGa0Oj2BztGi86v5SbFD0oVvnXIV8dZWFAQS16O-tyFVOcZXWvRQbByFb18Hhco5SgmkAQtPvk1ZD8lY8eLN9Q }, details: {
"ClientId": "mvc",
"ClientName": "MVC Client",
"GrantType": "authorization_code",
"AuthorizationCode": "4/KQGa0Oj2BztGi86v5SbFD0oVvnXIV8dZWFAQS16O-tyFVOcZXWvRQbByFb18Hhco5SgmkAQtPvk1ZD8lY8eLN9Q",
"Raw": {
"grant_type": "authorization_code",
"code": "4/KQGa0Oj2BztGi86v5SbFD0oVvnXIV8dZWFAQS16O-tyFVOcZXWvRQbByFb18Hhco5SgmkAQtPvk1ZD8lY8eLN9Q",
"redirect_uri": "http://localhost:57547/signin-idsrv",
"client_id": "mvc",
"client_secret": "***REDACTED***"
}
}
Am I doing anything wrong ?
Thank you.

Seems to be authorization_code grant is not suitable to this situation.
When I changed authorization_code to hybrid grant, it works.
Actually, I changed:
authorizationCodeClient.AllowedGrantTypes = new List<string>();
authorizationCodeClient.AllowedGrantTypes.Add(GrantType.AuthorizationCode);
To
authorizationCodeClient.AllowedGrantTypes = GrantTypes.Hybrid;
Using https://oidcdebugger.com/ to generate querystring:
http://localhost:57547/connect/authorize?client_id=mvc&redirect_uri=http%3A%2F%2Flocalhost%3A4300&scope=api1%20offline_access%20openid&response_type=code%20token%20id_token&response_mode=fragment&nonce=556bs2pazg2.
Magically, I can sign in the system using account & password and Google account.
Hope this helps someone stucks in IdentityServer4 as I did.

Related

IdentityServer4 and external oauth privider: The oauth state was missing or invalid

I'm trying to implement external oauth authentication within IdentityServer4.
All auth requests goes successfully. I have a message AuthenticationScheme: Identity.External signed in. in app log.
But when authentication process tries to go back to ExternalLoginCallback action it falls with Error 500 (The oauth state was missing or invalid) after HTTP 302.
Screenshot is here
Result 302. Request https://localhost:5999/Account/ExternalLoginCallback?state=xxx&code=xxx&session_state=xxx
Then request goes to https://localhost:5999/Account/ExternalLoginCallback (without any parametres)
My IS4 Startup
IdentityServerConfiguration.AddIdentityServer(services, _configuration);
services
.AddAuthentication()
.AddTinkoff(_configuration)
.AddSber(_configuration)
.AddEsia(_configuration);
AddTinkoff extension method:
public static AuthenticationBuilder AddTinkoff(this AuthenticationBuilder builder, IConfiguration config)
{
return builder.AddOAuth("TinkoffId", "Tinkoff ID", options =>
{
options.AuthorizationEndpoint = "https://id.tinkoff.ru/auth/authorize?response_type=code";
options.TokenEndpoint = "https://id.tinkoff.ru/auth/token?grant_type=authorization_code";
options.UserInformationEndpoint = "https://id.tinkoff.ru/userinfo/userinfo";
options.CallbackPath = "/Account/ExternalLoginCallback";
options.ClientId = "xxx";
options.ClientSecret = "xxxx";
options.SaveTokens = true;
options.SignInScheme = IdentityConstants.ExternalScheme;
options.BackchannelHttpHandler = new TinkoffAuthorizingHandler(new HttpClientHandler(), options);
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "name");
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email);
options.ClaimActions.MapAll();
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Post, context.Options.UserInformationEndpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
request.Headers.Add(
HttpRequestHeader.ContentType.ToString(),
"application/x-www-form-urlencoded"
);
request.Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>() {
new KeyValuePair<string, string>("client_id",options.ClientId),
new KeyValuePair<string, string>("client_secret",options.ClientSecret)
});
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user.RootElement);
},
//OnTicketReceived = async context =>
//{
// context.HttpContext.User = context.Principal;
// //context.SkipHandler();
//}
};
//options.Scope.Add("profile");
//options.Scope.Add("email");
//options.Scope.Add("phone");
});
}
My ExternalLogin action:
[HttpPost]
[HttpGet]
[AllowAnonymous]
public IActionResult ExternalLogin(string provider, string returnUrl = null)
{
_logger.LogInformation($"External login fired. ReturnUrl='{returnUrl}'. Provider='{provider}'");
var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Challenge(properties, provider);
}
ExternalLoginCallback action not fired while debug.
What I'm doing wrong. Why request redirects to itself without params?
Thank you.
I solved an issue for me.
I added
OnTicketReceived = async context =>
{
context.HttpContext.User = context.Principal;
//context.SkipHandler();
context.ReturnUri += $"/{context.Request.QueryString}";
}
Now it works as expected

.NET Core 3.1 IdentityServer4: getting invalid access token when using Resource Owner Password Credentials grant

I'm trying to get an access token from Identity Provider using Resource Owner Password Credentials grant type. The same configuration worked for .NET Core 2.2, but it doesn't work anymore for .NET Core 3.1. Here is the configuration of Identity Provider:
public class Startup
{
public IConfiguration Configuration { get; }
private readonly string _MyAllowSpecificOrigins = "fooorigin";
private readonly IWebHostEnvironment _env;
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
IdentityModelEventSource.ShowPII = true;
_env = env;
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddPersistence(Configuration); //Custom extension
services.AddAutoMapper(Assembly.GetAssembly(typeof(BaseMappingProfile)));
#region Options
services.Configure<IdentityServerOptions>(Configuration.GetSection("IdentityServerOptions"));
services.Configure<Settings>(Configuration.GetSection("Settings"));
#endregion
#region Configurations
services.AddTransient<IdentityServerOptions>();
#endregion
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<ITokenManagerHelper, TokenManagerHelper>();
services.AddScoped<IUserService, UserService>();
services.AddCors(options =>
{
options.AddPolicy(_MyAllowSpecificOrigins,
builder =>
{
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddMvc().AddFluentValidation(fv =>
{
fv.RegisterValidatorsFromAssemblyContaining<CommonValidator>();
fv.ImplicitlyValidateChildProperties = true;
})
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
var identityServerDataDBConnectionString = Configuration.GetConnectionString("IdentityServerConfigDatabase");
var migrationsAssembly = typeof(UsersDbContext).GetTypeInfo().Assembly.GetName().Name;
var identityAuthority = Configuration.GetValue<string>("IdentityServerOptions:Authority");
// Add Authentication
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = Configuration.GetValue<string>("IdentityServerOptions:Authority");
options.ClientId = Configuration.GetValue<string>("IdentityServerOptions:ClientName");
options.ClientSecret = Configuration.GetValue<string>("IdentityServerOptions:ClientSecret");
options.ResponseType = Configuration.GetValue<string>("IdentityServerOptions:ResponseType");
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("roles");
options.Scope.Add("fooapi");
options.Scope.Add("fooidentityapi");
options.Scope.Add("offline_access");
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.Remove("amr");
options.ClaimActions.DeleteClaim("sid");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.GivenName,
RoleClaimType = JwtClaimTypes.Role,
};
});
services.AddTransient<IPersistedGrantStore, PersistedGrantStore>();
services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
// Add Identity Server
// Add Signing Certificate
// Add Users Store
// Add Configurations Store
// Add Operational Stores
if (_env.IsDevelopment() || _env.IsStaging())
{
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddUserStore()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
{
builder.UseSqlServer(identityServerDataDBConnectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
};
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
{
builder.UseSqlServer(identityServerDataDBConnectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
};
})
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>();
}
else
{
//Todo: add certificate
}
}
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
IOptions<Settings> settingOptions)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseCors(_MyAllowSpecificOrigins);
app.UseCookiePolicy();
var forwardOptions = new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
RequireHeaderSymmetry = false
};
forwardOptions.KnownNetworks.Clear();
forwardOptions.KnownProxies.Clear();
app.UseForwardedHeaders(forwardOptions);
app.UseAuthentication();
app.UseRouting();
app.UseIdentityServer();
}
}
And here's the configuration of API:
public class Startup
{
#region Private Fields
private readonly string _allowedOrigins = "fooorigin";
#endregion
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthorization();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration.GetValue<string>("IdentityServerOptions:Authority");
options.RequireHttpsMetadata = false;
options.ApiName = Configuration.GetValue<string>("IdentityServerOptions:ApiName");
options.SupportedTokens = IdentityServer4.AccessTokenValidation.SupportedTokens.Jwt;
});
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
#region Options
services.AddOptions();
#endregion
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddCors(options =>
{
options.AddPolicy(_allowedOrigins, builder =>
{
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
});
services.AddMvc(o =>
{
o.EnableEndpointRouting = false;
o.Conventions.Add(new ApiExplorerGroupPerVersionConvention());
o.Filters.Add(new ModelStateFilter());
}).AddFluentValidation(fv =>
{
fv.RegisterValidatorsFromAssemblyContaining<CommonValidator>();
fv.ImplicitlyValidateChildProperties = true;
})
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
#region Customise default API behavour
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
#endregion
#region Versioning
services.AddApiVersioning(o =>
{
o.ApiVersionReader = new HeaderApiVersionReader("api-version");
o.DefaultApiVersion = new ApiVersion(1, 0);
o.AssumeDefaultVersionWhenUnspecified = true;
o.ReportApiVersions = true;
});
#endregion
#region Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1.0", new OpenApiInfo
{
Title = Configuration.GetValue<string>("SwaggerDocOptions:Title"),
Version = Configuration.GetValue<string>("SwaggerDocOptions:Version"),
Description = Configuration.GetValue<string>("SwaggerDocOptions:Description")
});
c.OperationFilter<RemoveApiVersionFromParamsOperationFilter>();
var basePath = PlatformServices.Default.Application.ApplicationBasePath;
var xmlPath = Path.Combine(basePath, "foo.xml");
c.IncludeXmlComments(xmlPath);
var scopes = Configuration.GetValue<string>("IdentityServerOptions:RequiredScopes").Split(',').ToList();
var scopesDictionary = new Dictionary<string, string>();
foreach (var scope in scopes)
{
scopesDictionary.Add(scope, scope);
}
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.OAuth2,
Scheme = "Bearer",
Flows = new OpenApiOAuthFlows
{
Password = new OpenApiOAuthFlow
{
TokenUrl = new Uri(Configuration.GetValue<string>("IdentityServerOptions:TokenEndpoint")),
Scopes = scopesDictionary
}
},
In = ParameterLocation.Header
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
Name = "Bearer",
In = ParameterLocation.Header
},
new List<string>()
}
});
});
#endregion
}
/// <summary>
/// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/// </summary>
/// <param name="app">Application builder</param>
/// <param name="env">Web host environment</param>
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseCors(_allowedOrigins);
app.UseAuthentication();
app.UseSerilogRequestLogging();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
app.UseSwagger(
o =>
{
o.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
{
var paths = new OpenApiPaths();
foreach (var x in swaggerDoc.Paths)
{
var key = x.Key.Contains("{version}") ? x.Key.Replace("{version}", swaggerDoc.Info.Version) : x.Key;
paths.Add(key, x.Value);
}
swaggerDoc.Paths = paths;
swaggerDoc.Extensions.Add(
new KeyValuePair<string,
IOpenApiExtension>("x-identity-authority",
new OpenApiString(Configuration.GetValue<string>("IdentityServerOptions:Authority"))));
});
o.RouteTemplate = "docs/{documentName}/swagger.json";
});
app.UseSwaggerUI(
c =>
{
c.SwaggerEndpoint("/docs/v1.0/swagger.json", "Foo API");
c.OAuthClientId(Configuration.GetValue<string>("IdentityServerOptions:ClientName"));
c.OAuthClientSecret(Configuration.GetValue<string>("IdentityServerOptions:ClientSecret"));
}
);
}
}
Now let's look at the process of getting an access token:
When I press "Authorize", it's validating and gets a token:
but when I try to access API resource which requires an authorization, it returns 401 error:
I tried to check the same in the Postman and when I try to access token endpoint it returns the access token like that:
I've been working for hours, tried many things but nothing worked. I tried to provide everything that can be cause of this problem, any help will be appreciated, thanks in advance.
After researching I found out that the ApiName must be the same as the name of the audience, also we should configure clients for JWT tokens, not the reference tokens.

How to get OAuth2 OpenId token from Identity Server 4 for client that uses OWIN

I am using IdentityServer4 to get my OpenId tokens for my web app.
But my web app uses Owin for security. I cannot find any example samples of where this is done.
So this code works;
In my client;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using SIR.API.Caller.Helpers;
namespace SIR.API.Caller
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = Settings.SignInAsAuthenticationType // "Cookies";
});
app.UseOpenIdConnectAuthentication(openIdConnectOptions: new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
Authority = Settings.AuthorityUrl1, //ID Server, "https://localhost:44314/"; https://localhost:44307/
ClientId = Settings.ClientId, // "SIR"
Scope = Settings.Scope, // "openid profile";
ResponseType = Settings.ResponseType, // "id_token code";
SignInAsAuthenticationType = Settings.SignInAsAuthenticationType,
//--------------------------------------// "Cookies";
RedirectUri = Settings.RedirectUri, // URL of website, http://localhost:50000/signin-oidc;
//RedirectUri = Settings.RedirectUri1, // URL of website, http://localhost:53200/signin-oidc;
RequireHttpsMetadata = Settings.RequireHttpsMetadata,
//--------------------------------------// true
ClientSecret = "secret"
});
app.Use(async (ctx, next) =>
{
var message = ctx.Authentication.User.Identity.IsAuthenticated
? $"User: {ctx.Authentication.User.Identity.Name}"
: "User Not Authenticated";
await next();
});
}
}
}
In my ID 4 server the startup is;
using System.Security.Cryptography.X509Certificates;
using IdentityServer4;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Mulalley.IdentityServer4.Helpers;
using QuickstartIdentityServer;
namespace Mulalley.IdentityServer4
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
//.AddDeveloperSigningCredential()
.AddSigningCredential(new X509Certificate2(Settings.CertPath, Settings.Password))
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers());
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
// register your IdentityServer with Google at https://console.developers.google.com
// enable the Google+ API
// set the redirect URI to http://localhost:port/signin-google
options.ClientId = "copy client ID from Google here";
options.ClientSecret = "copy client secret from Google here";
})
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://demo.identityserver.io/";
options.ClientId = "implicit";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseIdentityServer();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
}
and the Config.cs is;
using IdentityServer4;
using IdentityServer4.Models;
using IdentityServer4.Test;
using System.Collections.Generic;
using System.Security.Claims;
namespace QuickstartIdentityServer
{
public class Config
{
// scopes define the resources in your system
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("SIR", "Service Inspection Report")
};
}
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
var baseUri = "http://localhost:53200/";
// client credentials client
return new List<Client>
{
new Client
{
ClientId = "client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "SIR" },
AlwaysIncludeUserClaimsInIdToken = true
},
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "SIR" },
AlwaysIncludeUserClaimsInIdToken = true
},
// OpenID Connect hybrid flow and client credentials client (MVC)
new Client
{
ClientId = "SIR",
ClientName = "SIR",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { baseUri + "signin-oidc" },
PostLogoutRedirectUris = { baseUri + "signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"SIR"
},
AllowOfflineAccess = true,
AlwaysIncludeUserClaimsInIdToken = true
}
};
}
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password",
Claims = new List<Claim>
{
new Claim("name", "Alice"),
new Claim("website", "https://alice.com")
}
},
new TestUser
{
SubjectId = "2",
Username = "bob",
Password = "password",
Claims = new List<Claim>
{
new Claim("name", "Bob"),
new Claim("website", "https://bob.com")
}
}
};
}
}
}
And the question is how do I get the token in C# so I can have a look at it? And how do I set the GetClaimsFromUserInfoEndpoint = true which is not available in OWIN?
Are there any code samples I can look at with OWIN access to ID4?
EDIT: This code appears to be what I need: https://identitymodel.readthedocs.io/en/latest/client/token.html
However I put the code in and I get an access token which when I put it in jwt.io I get a message "Invalid Signature".
private static async Task<TokenResponse> GetClientCredentialsTokenResponse()
{
var client = new HttpClient();
var tokenRequest = new ClientCredentialsTokenRequest
{
Address = Settings.AuthorityUrl1 + "connect/token",
ClientId = Settings.ClientId,
ClientSecret = "secret",
Scope = "SIR"
};
return await client.RequestClientCredentialsTokenAsync(tokenRequest);
}
private static async Task<TokenResponse> GetTokenResponse()
{
var client = new HttpClient();
var tokenRequest = new TokenRequest
{
Address = Settings.AuthorityUrl1 + "connect/token",
GrantType = GrantType.ClientCredentials,
ClientId = Settings.ClientId,
ClientSecret = "secret",
Parameters =
{
{"scope", "SIR"}
}
};
return await client.RequestTokenAsync(tokenRequest);
}
private static void ThrowResponseException(TokenResponse response)
{
const string message = "Problem accessing the UserInfo endpoint";
if (response.Exception == null)
{
throw new Exception($"{message}: {response.Error}. {response.ErrorDescription}.");
}
throw new Exception(message, response.Exception);
}
}
}

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)

How can I use ASP.NET MVC Owin AccessToken in Google.Apis call?

I'm trying to use the AccessToken provided by Owin in Google.Apis requests but I'm receiveing the exception System.InvalidOperationException (Additional information: The access token has expired but we can't refresh it).
My configuration of Google Authentication is OK and I can successfully login into my application with it. I store the context.AccessToken as a Claim in the authentication callback (OnAuthenticated "event" of GoogleOAuth2AuthenticationProvider).
My Startup.Auth.cs configuration (app.UseGoogleAuthentication(ConfigureGooglePlus()))
private GoogleOAuth2AuthenticationOptions ConfigureGooglePlus()
{
var goolePlusOptions = new GoogleOAuth2AuthenticationOptions()
{
ClientId = "Xxxxxxx.apps.googleusercontent.com",
ClientSecret = "YYYYYYzzzzzz",
Provider = new GoogleOAuth2AuthenticationProvider()
{
OnAuthenticated = context =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("Google_AccessToken", context.AccessToken));
return Task.FromResult(0);
}
},
SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie
};
goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/plus.login");
goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/userinfo.email");
return goolePlusOptions;
}
The code in which the exception is throwed (Execute() method)
var externalIdentity = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
var accessTokenClaim = externalIdentity.FindAll(loginProvider + "_AccessToken").First();
var secrets = new ClientSecrets()
{
ClientId = "Xxxxxxx.apps.googleusercontent.com",
ClientSecret = "YYYYYYzzzzzz"
};
IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = secrets,
Scopes = new[] { PlusService.Scope.PlusLogin, PlusService.Scope.UserinfoEmail }
});
UserCredential credential = new UserCredential(flow, "me", new TokenResponse() { AccessToken = accessTokenClaim.Value });
var ps = new PlusService(
new BaseClientService.Initializer()
{
ApplicationName = "My App Name",
HttpClientInitializer = credential
});
var k = ps.People.List("me", PeopleResource.ListRequest.CollectionEnum.Visible).Execute();
Is there another way to get the original AccessToken or refresh it without pass thru the entire authentication process (the user is already authenticated)?
I need to query some GooglePlus profile data such as GivenName, familyName, gender, profile picture and profile url.
Linda helped me with an URL pointing to a new asp.net mvc sample (https://codereview.appspot.com/194980043/).
I just had to add AccessType = "offline" to GoogleOAuth2AuthenticationOptions and save some extra info to create a new instance of TokenResponse when I need.
Google Authentication Options
private GoogleOAuth2AuthenticationOptions ConfigureGooglePlus()
{
var goolePlusOptions = new GoogleOAuth2AuthenticationOptions()
{
AccessType = "offline",
ClientId = "Xxxxxxx.apps.googleusercontent.com",
ClientSecret = "Yyyyyyyyyy",
Provider = new GoogleOAuth2AuthenticationProvider()
{
OnAuthenticated = context =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("Google_AccessToken", context.AccessToken));
if (context.RefreshToken != null)
{
context.Identity.AddClaim(new Claim("GoogleRefreshToken", context.RefreshToken));
}
context.Identity.AddClaim(new Claim("GoogleUserId", context.Id));
context.Identity.AddClaim(new Claim("GoogleTokenIssuedAt", DateTime.Now.ToBinary().ToString()));
var expiresInSec = (long)(context.ExpiresIn.Value.TotalSeconds);
context.Identity.AddClaim(new Claim("GoogleTokenExpiresIn", expiresInSec.ToString()));
return Task.FromResult(0);
}
},
SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie
};
goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/plus.login");
goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/plus.me");
goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/userinfo.email");
return goolePlusOptions;
}
How to retrieve the credentials (using token info "stored" as claim)
private async Task<UserCredential> GetCredentialForApiAsync()
{
var initializer = new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "Xxxxxxxxx.apps.googleusercontent.com",
ClientSecret = "YYyyyyyyyyy",
},
Scopes = new[] {
"https://www.googleapis.com/auth/plus.login",
"https://www.googleapis.com/auth/plus.me",
"https://www.googleapis.com/auth/userinfo.email" }
};
var flow = new GoogleAuthorizationCodeFlow(initializer);
var identity = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ApplicationCookie);
var userId = identity.FindFirstValue("GoogleUserId");
var token = new TokenResponse()
{
AccessToken = identity.FindFirstValue("Google_AccessToken"),
RefreshToken = identity.FindFirstValue("GoogleRefreshToken"),
Issued = DateTime.FromBinary(long.Parse(identity.FindFirstValue("GoogleTokenIssuedAt"))),
ExpiresInSeconds = long.Parse(identity.FindFirstValue("GoogleTokenExpiresIn")),
};
return new UserCredential(flow, userId, token);
}

Resources