.Net-Core Log out of Oauth - c#

I've created an application which uses OAuth to login to Coinbase. My startup configuration looks like so:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = COINBASE_AUTH_ID;
})
.AddCookie()
.AddOAuth(COINBASE_AUTH_ID, options =>
{
options.ClientId = Configuration["Coinbase:ClientId"];
options.ClientSecret = Configuration["Coinbase:ClientSecret"];
options.CallbackPath = new PathString("/signin-coinbase");
options.AuthorizationEndpoint = "https://www.coinbase.com/oauth/authorize?meta[send_limit_amount]=1";
options.TokenEndpoint = "https://api.coinbase.com/oauth/token";
options.UserInformationEndpoint = "https://api.coinbase.com/v2/user";
COINBASE_SCOPES.ForEach(scope => options.Scope.Add(scope));
options.SaveTokens = true;
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
options.ClaimActions.MapJsonKey("urn:coinbase:avatar", "avatar_url");
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
request.Headers.Add("CB-VERSION", DateTime.Now.ToShortDateString());
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = JObject.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user);
}
};
});
When a user logs in I return a challenge result and let the default authentication do it's work.
[HttpGet]
public IActionResult Login(string returnUrl = "/")
{
return Challenge(new AuthenticationProperties() { RedirectUri = returnUrl });
}
I'm trying to figure out how to logout but when I call Signout on the base controller nothing is happening.
[HttpGet]
public IActionResult Logout()
{
this.SignOut();
return Redirect(Url.Content("~/"));
}
How can I log out of oauth?

There's probably a more graceful way of doing this but for now. I found I can call the SignOut method on the HTTPContext
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
return Redirect(Url.Content("~/"));
}

Related

IdentityServer4 Session Cookie Management (how to do it properly?)

We are working on an idetityserver4 (A SPA application in angular) that will run on a standalone server and will comunicate with an API(asp.net API) that is on another server, the patern we are trying to implement is BFF (backend for front end) and if we didn't misunderstand the concept badly, our ID4 will act as the gateway to the API, firstly we log to the ID4 with the ClientID and secret so that we get a token and generate the session cookies, afterward we forward everything to the API to complete the local login.
Everything works fine till here, we get the response we want, token gets set to acces the API and cookies generated for that client are automatically returned in Header (we use POSTMAN), but when i try to make a request to the API (request that firstly goes to ID4 to get verified if the client cookie is not expired) apparently ID4 does not verify if the client cookie is or not expired, i can make as many requests as i like....
We really don't understand how to do it properly and we didn't find something that can help us...
Here are the snippets
Startup.cs
services.AddAuthentication(options =>
{
options.DefaultScheme = "cookies";
options.DefaultChallengeScheme = "oidc";
options.RequireAuthenticatedSignIn = true;
})
.AddCookie("cookies", options =>
{
options.Cookie.Name = "cookie-bff";
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(1);
options.Cookie.SecurePolicy = 0;
options.SlidingExpiration = true;
options.Events.OnSigningOut = async e =>
{
// revoke refresh token on sign-out
await e.HttpContext.RevokeUserRefreshTokenAsync();
};
})
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:4001/";
options.ClientId = "angular-client";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
/*
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("api");
options.Scope.Add("offline_access");
*/
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
services.AddAccessTokenManagement(options =>
{
// client config is inferred from OpenID Connect settings
// if you want to specify scopes explicitly, do it here, otherwise the scope parameter will not be sent
options.Client.Scope = "write";
})
.ConfigureBackchannelHttpClient();
services.AddUserAccessTokenClient("user_client", client =>
{
client.BaseAddress = new Uri("https://localhost:5001/api/");
});
services.AddClientAccessTokenClient("client", configureClient: client =>
{
client.BaseAddress = new Uri("https://localhost:5001/api/");
});
Where we configure the Context for operation and configuration
services.AddIdentityServer(options => {
options.Authentication.CheckSessionCookieName = "cookie-bff";
options.Authentication.CookieLifetime = TimeSpan.FromMinutes(1);
options.Authentication.CookieSameSiteMode = SameSiteMode.Strict;
options.Authentication.CookieSlidingExpiration = true;
options.LowerCaseIssuerUri = false;
options.EmitScopesAsSpaceDelimitedStringInJwt = false;
})
.AddDeveloperSigningCredential()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(configuration.GetConnectionString("LocalDevelopment"),
sql => sql.MigrationsAssembly(migrationAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(configuration.GetConnectionString("LocalDevelopment"),
sql => sql.MigrationsAssembly(migrationAssembly));
});
This is our LoginController(its work in progres, we are beginers)
[HttpPost]
[Route("/identityserver/login")]
public async Task<IActionResult> Post([Required, FromBody] LoginPostDTO Json)
{
var client = new HttpClient();
var response = await client.RequestTokenAsync(new TokenRequest
{
Address = "https://localhost:4001/connect/token",
GrantType = "client_credentials",
ClientId = "angular-client",
ClientSecret = "secret",
Parameters =
{
{ "scope", "read"},
{"openid","profile"}
}
});
var claims = new List<Claim>
{
new Claim(JwtClaimTypes.Name,"Company"),
new Claim(JwtClaimTypes.Role, "Administrator"),
new Claim(JwtClaimTypes.Subject, "Company")
};
var claimsIdentity = new ClaimsIdentity(claims, "cookies");
var authProperties = new AuthenticationProperties
{
AllowRefresh = true,
// Refreshing the authentication session should be allowed.
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(1),
// The time at which the authentication ticket expires. A
// value set here overrides the ExpireTimeSpan option of
// CookieAuthenticationOptions set with AddCookie.
IsPersistent = false,
// Whether the authentication session is persisted across
// multiple requests. When used with cookies, controls
// whether the cookie's lifetime is absolute (matching the
// lifetime of the authentication ticket) or session-based.
IssuedUtc = DateTime.Now,
// The time at which the authentication ticket was issued.
RedirectUri = "https://localhost:4001/signin-oidc"
// The full path or absolute URI to be used as an http
// redirect response value.
};
await HttpContext.SignInAsync("cookies", new ClaimsPrincipal(claimsIdentity), authProperties);
var stringContent = new StringContent(JsonConvert.SerializeObject(Json), Encoding.UTF8, "application/json");
var api = new HttpClient();
api.SetBearerToken(response.AccessToken);
var apiResponse = await api.PostAsync("https://localhost:5001/api/login", stringContent);
return Ok(apiResponse.Content.ReadAsAsync(typeof(JObject)).Result);
}
}
And here is the Controller where we comunicate with the API after we login
public class Controller : ControllerBase
{
[HttpPost]
[Route("/identityserver/request")]
[Produces("application/json")]
[Consumes("application/json")]
public async Task<IActionResult> Post([Required, FromBody]DTO Json)
{
var stringContent = new StringContent(JsonConvert.SerializeObject(Json), Encoding.UTF8, "application/json");
var api = new HttpClient();
api.SetBearerToken(await HttpContext.GetClientAccessTokenAsync());
var apiResponse = await api.PostAsync("https://localhost:5001/api/request/medical-request", stringContent);
return Ok(apiResponse.Content.ReadAsAsync(typeof(JObject)).Result);
}
}
Thank you in advance!
P.S This is my first post on stack :)

AspNetCore API JWT Token works in PostMan but not with HttpClient

I am trying to call an API with HttpClient, and when I call the API without the [Authorize] I get all of the information correctly. when I add the attribute I do get a 401 unauthorized error. The funny thing is it works correctly when I call with the Bearer token from Postman.
I have seen a few posts where they say to add app.UseAuthentication(); before app.UseAuthorization(); which I have done.
Below is the code that I use to call the API, something I noticed is I am calling http://localhost:5000/api/accounts but I am getting a response from https://localhost:5001/api/accounts
protected HttpClient CreateClient()
{
var client = new HttpClient { BaseAddress = new Uri(ConfigurationManager.AppSettings["ServerAddress"]) };
if(_securityService.HasToken)
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _securityService.Token);
}
return client;
}
protected async Task<T> GetAsync<T>(string url, CancellationToken cancellationToken = default, params object[] parameters)
{
using var client = CreateClient();
if (parameters.Any())
url = string.Format(url, parameters);
var response = await client.GetAsync(url, cancellationToken);
if (response.IsSuccessStatusCode)
{
var stringContent = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(stringContent);
}
else
{
//TODO: Add Logger and log Result
return default;
}
}
Client ApiAddress
Response Message
I have 2 questions here, why would this change a request uri, can someone please explain why this would work in postman, but not using HttpClient.
I thought about this and thought I should add the server code in here too.
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<SchedulerContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
var securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Constants.ConstantsToAddToSecrets.Seceret));
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = securityKey,
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddAuthorization(options =>
{
options.DefaultPolicy =
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
options.AddPolicy(Roles.Manager, policy => policy.RequireClaim(Roles.Manager));
options.AddPolicy(Roles.User, policy => policy.RequireClaim(Roles.User));
options.AddPolicy(Roles.Admin, policy => policy.RequireClaim(Roles.Admin));
options.InvokeHandlersAfterFailure = true;
});
services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 6;
options.User.RequireUniqueEmail = false;
options.Tokens.AuthenticatorIssuer = ConstantsToAddToSecrets.Issuer;
});

Configure the return_type field in ASP.NET Core

I'm using the AuthenticationBuilder.AddOAuth(...) method to add OAuth functionality to my site. This is working perfectly for one service I am connecting to, but the same doesn't go for the second service.
Apparently, during the redirect to the authorization endpoint, the second service requires the URL parameter return_type to be set to token, my configuration sets it to code though. I haven't been able to find what part of the OAuthOptions determines this parameter so far.
I'll add the code I am using for convenience:
authBuilder.AddOAuth(Identifier, options =>
{
options.ClientId = ClientId;
options.ClientSecret = ClientSecret;
options.CallbackPath = new PathString($"/signin-{Identifier}");
options.AccessDeniedPath = new PathString("/");
options.AuthorizationEndpoint = Domain + AuthorizationEndpoint;
options.TokenEndpoint = Domain + TokenEndpoint;
options.UserInformationEndpoint = Domain + UserInfoEndpoint;
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, NameIdentifierKey);
options.ClaimActions.MapJsonKey(ClaimTypes.Name, NameKey);
options.SaveTokens = true;
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
context.RunClaimActions(user.RootElement);
}
};
options.Scope.Add(Scope);
});

_signInManager.ExternalLoginSignInAsync returns null when using LinkedIn authentication

I'm using ASP.NET Core 2.1 and I want to use LinkedIn authentication.
Because there is no extension method .AddLinkedIn by default (developed by Microsoft's dev), so I've tried:
services.AddOAuth("LinkedIn", options =>
{
options.ClientId = Configuration["Authentication:LinkedIn:ClientId"];
options.ClientSecret = Configuration["Authentication:LinkedIn:ClientSecret"];
options.Scope.Add("r_basicprofile");
options.Scope.Add("r_emailaddress");
options.CallbackPath = "/signin-linkedin";
options.AuthorizationEndpoint = "https://www.linkedin.com/oauth/v2/authorization";
options.TokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken";
options.UserInformationEndpoint = "https://api.linkedin.com/v1/people/~:(id,formatted-name,email-address,picture-url)";
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
using (var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
request.Headers.Add("x-li-format", "json");
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
string responseText = await response.Content.ReadAsStringAsync();
var user = Newtonsoft.Json.Linq.JObject.Parse(responseText);
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Value<string>("id")));
context.Identity.AddClaim(new Claim(ClaimTypes.Name, user.Value<string>("formattedName")));
context.Identity.AddClaim(new Claim(ClaimTypes.Email, user.Value<string>("emailAddress")));
context.Identity.AddClaim(new Claim("picture", user.Value<string>("pictureUrl")));
}
}
}
};
});
and in the controller, I've tried to get the info:
var info = await _signInManager.GetExternalLoginInfoAsync();
it returned null.
I'd tried to clear all cookies and signed in again with another services like Facebook, Google or Twitter. All of them worked perfectly (I got the information) except LinkedIn. Why?
I'd tried to rewrite the code and change the line:
using (var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint))
to
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
Then, it should work! (I don't know why).
It seems like the using block cannot be accepted during this action. After closing the scope, all of the claims will be cleard.

Open Identity Server 4 Unauthorized UserInfo

I'm currently struggling with OpenIdentityServer 4 in ASP Core 1.1.
I'm able to grant tokens, using ResourceOwnerPassword grant type etc. Created my custom ResourcePasswordValidator etc.
Currently in my test application I retrieve a token with user credentials and all issues fine, however when I try to access the IdentityController with an [Authorize] attribute I'm redirected to unauthorized page and sent a 403 forbidden http code
I'm not sure what the issue is. I suspect it could be from scope/resource issue
Any help whatsoever appreciated.
Sample code for consumer
public class TestAuthentication
{
private HttpClient _client;
public TestAuthentication()
{
_client = new HttpClient();
}
public async Task RunTest()
{
var token = await GetToken();
if (string.IsNullOrWhiteSpace(token)) return;
await GetClaims(token);
}
private async Task<string> GetToken()
{
var response = "";
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
//var tokenClient = new TokenClient(disco.TokenEndpoint, "EduOne", "secret");
//var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api");
var tokenClient = new TokenClient(disco.TokenEndpoint, "ro.client", "secret");
var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync("alice#mail.com", "Password1!", "api1");
// var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync("alice#mail.com", "Password1!", "openid");
if (tokenResponse.IsError)
{
Console.Out.WriteLine("Error:");
Console.Out.WriteLine(tokenResponse.Error);
Console.Out.Write(tokenResponse.ErrorDescription);
}
else
{
var extraClaims = new UserInfoClient(disco.UserInfoEndpoint);
var identityClaims = await extraClaims.GetAsync(tokenResponse.AccessToken);
response = tokenResponse.Json.ToString();
Console.Out.WriteLine($"token: {response}");
}
return response;
}
private async Task GetClaims(string token)
{
try
{
var obj = JObject.Parse(token);
var tok = obj["access_token"]?.ToString();
_client = new HttpClient();
_client.SetBearerToken(tok);
var response = await _client.GetAsync("http://localhost:5000/api/v1/identity");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
}
catch (Exception e)
{
var m = e.Message;
//throw;
}
}
~TestAuthentication()
{
_client = null;
}
}
Code for setups:
Client =>
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = {"api1" },
AccessTokenType = AccessTokenType.Reference
},
User =>
new TestUser
{
SubjectId = "1",
Username = "alice#mail.com",
Password = "Password1!",
Claims =
{
new Claim(JwtClaimTypes.Email, "mail#mail.com")
}
},
Resource =>
new IdentityResource("api1", new string[]{JwtClaimTypes.Email})
Startup =>
app.UseIdentityServer();
// app.UseIdentity();
// app.UseIdentity();
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
ApiSecret = "secret",
Authority = "http://localhost:5000",
RequireHttpsMetadata = false,
DiscoveryDocumentRefreshInterval = TimeSpan.FromMinutes(5),
ApiName = "FiserOpenIdentityApi",
SupportedTokens = IdentityServer4.AccessTokenValidation.SupportedTokens.Both,
AllowedScopes = { "openid", "profile", "email", "api1", "FiserOpenIdentityApi" }
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
Authority = "http://localhost:5000",
ClientId = "ro.client",
RequireHttpsMetadata = false,
ClientSecret = "secret",
SaveTokens = false
});
// app.UseJwtBearerAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(
name: "RESTApiV1",
template: "api/v1/{controller}/{action}/{id?}");
});
app.UseMongoDbForIdentityServer();
I am using Hybrid flow. i think you are missing authenticationHeader in HttpClient.
Please follow below code, its working and it may help you.
var client = new HttpClient();
var accessToken = await HttpContext
.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:44323");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await client.GetUserInfoAsync(new UserInfoRequest
{
Address = disco.UserInfoEndpoint,
Token = accessToken
});
var address = response.Claims.FirstOrDefault(c => c.Type == "address")?.Value;
var add = new AddressViewModel();
add.Address = address;
return View(add);

Categories