I've implemented Microsoft Identity platform in my Razore Pages application.
Almost everything works, except the redirect url AFTER user logout.
I let you see my configuration.
That is how I add authentication in my project:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(azureADSection)
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { scope })
.AddInMemoryTokenCaches();
An here how I add the authorization:
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
Then I want to override the default behaviour for logout:
Here my Signout button:
<a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignOut">Sign out</a>
Account is not a control of mine. You can find the controller here.
The logout works. The guide says:
call Signout(), which lets the OpenId connect middleware contact the Microsoft identity platform logout endpoint which:
clears the session cookie from the browser,
and finally calls back the logout URL, which, by default, displays the signed out view page >SignedOut.html also provided as part of ASP.NET Core.
In fact, I am redirected to SignedOut.html.
The guide does not explain how I can override that behavior but it gives me a tip. I have not intercepted the event how it is written in the guide, but I have overriden two properties:
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SignedOutCallbackPath = "/test";
//options.SignedOutRedirectUri = "/test";
//options.SignedOutRedirectUri = "https://www.google.com";
});
But my solution does not works. It still redirect to default page when I am logged out. How can I customize the after logout url?
Thnak you
Please check if you can try to use custom URL Rewriting Middleware to redirect based on checking the path .Add this before app.UseMvc in startup.cs under you can redirect to your own custom signout page if you wish.
app.UseRewriter(
new RewriteOptions().Add(
context => { if (context.HttpContext.Request.Path == "/MicrosoftIdentity/Account/SignedOut")
{ context.HttpContext.Response.Redirect("/Index"); }
})
);
Or
If controller is present a workaround is to build you own AccountController :
public class AccountController : Controller
{
[HttpGet]
public IActionResult SignIn()
{
var redirectUrl = Url.Action(nameof(HomeController.Index), "Home");
return Challenge(
new AuthenticationProperties { RedirectUri = redirectUrl },
OpenIdConnectDefaults.AuthenticationScheme);
}
[HttpGet]
public IActionResult SignOut()
{
var callbackUrl = Url.Action(nameof(SignedOut), "Account", values: null, protocol: Request.Scheme);
return SignOut(
new AuthenticationProperties { RedirectUri = callbackUrl },
CookieAuthenticationDefaults.AuthenticationScheme,
OpenIdConnectDefaults.AuthenticationScheme);
}
[HttpGet]
public IActionResult SignedOut()
{
if (User.Identity.IsAuthenticated)
{
// Redirect to home page if the user is authenticated.
return RedirectToAction(nameof(HomeController.Index), "Home");
}
return RedirectToAction(nameof(HomeController.Index), "ThePathtoredirect");
}
References:
customize azure ad sign out page -SO Reference
define signedout page-SO Reference
Above example will work for MicrosoftIdentity if decorated with the right route:
[Area("MicrosoftIdentity")]
[Route("[area]/[controller]/[action]")]
Related
I'm trying to implement OpenId and oidc-client-js in react. Right now I'm stuck in implementing logout function.
From what I understand, I need to set the post_logout_redirect_uri and use signoutRedirect() to logout the user. Logging out the user works, but unfortunately it stays in the identity server logout page. What I need to do is to redirect the user to the post_logout_redirect_uri.
Can someone tell me what am I missing here? Thanks in advance!
This is the URL where I get redirected. https://identityserver.xx.com/Account/Logout?logoutId=CfDJ8Cqm6alCoddAqWl...
Here's my tech stack:
Identity Server v4
oidc-client-js
ReactJS (TS) with mobx state manager.
Below is my Identity server admin settings.
Front Channel Logout Uri: http://localhost:3000/signout-oidc
Post Logout Redirect Uris: http://localhost:3000/signout-callback-oidc
Here's the logout code
logout = async () => {
try {
userManager.signoutRedirect({
id_token_hint: this.user?.id_token,
post_logout_redirect_uri : process.env.REACT_APP_LOGOFF_REDIRECT_URL,
state: this.user?.state
}).then( () => {
console.log('done signoutRedirect')
});
userManager.clearStaleState()
}catch(error){
console.log(error);
}
}
in AccountController -> BuildLoggedOutViewModelAsync method check AutomaticRedirectAfterSignOut is true when constructing the viewmodel.
var vm = new LoggedOutViewModel
{
AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut, //this must return true.
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
SignOutIframeUrl = logout?.SignOutIFrameUrl,
LogoutId = logoutId
};
in your view LoggedOut.cshtml check ~/js/signout-redirect.js is included properly.
#section scripts
{
#if (Model.AutomaticRedirectAfterSignOut)
{
<script src="~/js/signout-redirect.js"></script>
}
}
if your logged out page doesn't contain anchor tag with <a id="post-logout-redirect-uri" ...> you probably have mismatching post_logout_redirect_uri configured in request and client.
I followed a link to achieve google SSO github.com/aspnet/Security/issues/1370. But even after successful login it is taking me to redirect uri mentioned in authentication property. It is not taking to the callback url. Could someone help on this? Our application is a .net core 3.1 with IdentityServer4.
Am expecting signinoauth2 API to be hit after google login, but thats not happening.
I could see a network call from browser with below format and getting correlation error.
https://localhost:44368/signinoauth2?state=&code=&scope=***&prompt=none
Exception: Correlation failed.
Show raw exception details
Exception: An error was encountered while handling the remote login.
Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler.HandleRequestAsync()
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Soulbook.Api.Startup+<>c+<b__5_1>d.MoveNext() in Startup.cs
await next.Invoke();
Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
PFB my code for reference,
[HttpGet]
[Authorize(AuthenticationSchemes = GoogleDefaults.AuthenticationScheme)]
[Route("/Feed")]
public ActionResult Feed()
{
return Ok();
}
[HttpGet]
[Route("/signin")]
public ActionResult SignIn()
{
var authProperties = new AuthenticationProperties
{
RedirectUri = "/"
};
return new ChallengeResult(GoogleDefaults.AuthenticationScheme, authProperties);
}
[HttpPost]
[Route("/signinoauth2")]
public ActionResult<LoginResponse> signinoauth2Async([FromForm]object data)
{
return Ok();
}
Startup.cs
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie(o => {
o.LoginPath = "/signin";
o.LogoutPath = "/signout";
o.ExpireTimeSpan = TimeSpan.FromDays(7);
})
.AddGoogle(o => {
o.ClientId = "***";
o.ClientSecret = "**";
o.SaveTokens = true;
o.CallbackPath = "/signinoauth2";
});
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(GoogleDefaults.AuthenticationScheme)
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
}).AddNewtonsoftJson();
EDIT: Having signinoauth2 in any one of the below formats also doesnt help.
[HttpGet]
[Route("/signinoauth2")]
public ActionResult<LoginResponse> signinoauth2Async(string state, string code, string scope, string prompt)
{
return Ok();
}
[HttpPost]
[Route("/signinoauth2")]
public ActionResult<LoginResponse> signinoauth2Async(string state, string code, string scope, string prompt)
{
return Ok();
}
I assume that you want to get Google user information in your enpoint?
Then what you have to do is configure the external authentication properties. And thanks to this you are going to be able to get the user on your redirect endpoint.
[HttpGet("login/google/")]
[AllowAnonymous]
public async Task<IActionResult> LoginGoogle()
{
var properties = _signInManager.ConfigureExternalAuthenticationProperties(GoogleDefaults.AuthenticationScheme, "/api/identity/google-redirect");
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
}
What you configured on startup is a callback route which gets handled by Middleware and never hits the endpoint in your controller. What you want to achive is get user on redirect route like this
[HttpGet("google-redirect")]
[AllowAnonymous]
public async Task<IActionResult> CallbackGoogle()
{
var info = await _signInManager.GetExternalLoginInfoAsync();
return Ok();
}
It sounds like you aren't actually being properly authenticated, if you were the app would redirect to the landing page whose controller I assume has an [Authorize] attribute. Could you have possibly forgotten to add yourself as a user in the db that your identity server is referencing?
I have an application https://app.example.com (home) and I have deep link working https://app.example.com/function/123 (direct_link) and navigating directly to direct_link works if the user is already authenticated.
We are using angular-oauth2-oidc and I can't find a way to initiate authentication and bring the user back to direct_link post authentication, it always returns to the home and I have paste the direct_link again in the address bar.
import { AuthConfig } from 'angular-oauth2-oidc';
export const authConfig: AuthConfig = {
// Url of the Identity Provider
issuer: 'https://cognito-idp.<region>.amazonaws.com/<id>',
// URL of the SPA to redirect the user to after login
redirectUri: window.location.origin,
// The SPA's id. The SPA is registerd with this id at the auth-server
clientId: '<id>',
// set the scope for the permissions the client should request
// The first three are defined by OIDC. The 4th is a usecase-specific one
scope: 'openid',
strictDiscoveryDocumentValidation: false,
responseType:'token',
oidc: true
}
export class AuthGuardService implements CanActivate{
constructor(private oauthService: OAuthService, private router: Router) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
if (this.oauthService.hasValidIdToken()) {
return true;
}
this.router.navigate(['home'], { queryParams: { returnUrl: state.url }});
return false;
}
}
export class HomeComponent implements OnInit {
returnUrl:string;
constructor(
private oauthService: OAuthService,
private router: Router) { }
login() {
this.oauthService.redirectUri = window.location.origin + this.returnUrl;
this.oauthService.initImplicitFlow();
}
logout() {
this.oauthService.logOut();
}
ngOnInit() {
}
}
We're using the angular-oauth2-oidc library with Azure AD B2C as well, and had a similar requirement.
Our deep linking requirements prevented us from using the redirectUri as the URL was dynamic (ie: product IDs included in the URL), and Azure AD B2C doesn't support wildcard redirectUris.
Our solution was to capture the current URL in session storage prior to invoking the oauthService's login flow, and then using that stored URL after the login is complete to redirect to the original URL, so for example:
export class AuthenticationService {
constructor(private storageService: SessionStorageService, private oauthService: OAuthService) { }
...
isLoggedIn(): boolean {
return this.oauthService.hasValidAccessToken();
}
...
login(): void {
this.oauthService.tryLoginImplicitFlow().then(success => {
if (!success) {
this.storageService.set('requestedUrl', location.pathname + location.search);
this.oauthService.initLoginFlow();
} else {
let requestedUrl = this.storageService.get('requestedUrl');
if (requestedUrl) {
sessionStorage.removeItem('requestedUrl');
location.replace( location.origin + requestedUrl);
}
}
This login method is part of our own auth service which mostly just delegates over to the OAuthService provided in the angular-oauth2-oidc package.
In our login method, we first attempt the tryLoginImplicitFlow() to see if the user has been authenticated.
If the tryLoginImplicitFlow() returns false, it means they aren't logged in, and we capture their current URL and shove it into session storage.
If it returns true, means they are authenticated, so we check to see if there is a stored URL, and if so, we redirect to it.
From a flow point of view, it works like this:
User attempts to access a deep link: /site/products/1234
App Component (not shown) checks the isLoggedIn() method of the auth service, and if not logged in, invokes the login() method
Login method tries the tryLoginImplicitFlow() (which does things like checking for a state hash in the URL), and it fails, so the method calls initLoginFlow()
User is redirected to some xxxx.b2clogin.com domain and logs in; B2C redirects the user to the root of our web app
App Component kicks in again and checks isLoggedIn(), which is still false, so calls the login() method
Login method tries the tryLoginImplicitFlow() (which picks up the fact that the user was just redirected from the B2C, and grabs the tokens) and it succeeds.
Login method checks session storage for the originally requested URL, sees it there, and redirects the user to that original page.
I know what you are thinking: "WOW! That's a whole lot of re-directs" ...and you are right - but it actually is surprisingly quick.
I'm looking at how to disconnect the user currently logged on the mvc client (e.g. http://localhost:5001), when that user performs logout on identity server's deployment (e.g. http://localhost:5000)
I understand there's an implementation of OAuth2 in identityserver4 that does just that (https://openid.net/specs/openid-connect-backchannel-1_0.html and https://openid.net/specs/openid-connect-frontchannel-1_0.html)
Luckily for me, Brock Allen just pushed a change in the samples less than a day ago: https://github.com/IdentityServer/IdentityServer4.Samples/issues/197
However the sample is either incomplete at this point, or I'm missing something.
on my server, I'm setting the value of FrontChannelLogoutUrl to http://localhost:5001/frontchannello, and I added that piece of code to my mvc client (basically stolen from the sample):
[HttpGet("frontChannello")]
public IActionResult FrontChannelLogout(string sid)
{
if (User.Identity.IsAuthenticated)
{
var currentSid = User.FindFirst("sid")?.Value ?? "";
if (string.Equals(currentSid, sid, StringComparison.Ordinal))
{
//await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return new SignOutResult(new[] { "Cookies", "oidc" });
}
}
return NoContent();
}
That code never gets called.
So my question is: should I use backchannel or frontchannel; and, how to implement it
The Identity server 4 documentation describes well how front-channel logout should be implemented. Look for the Quickstart 8_AspnetIdentity as it provides most of the code required for the implementation.
Some highlights of the code required in the identity server :
In the AccountController.cs, the Logout function builds a LoggedOutViewModel and returns a LoggedOut view.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
// build a model so the logged out page knows what to display
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
...
return View("LoggedOut", vm);
}
The SignOutIframeUrl iframe is served in the LoggedOut.cshtml.
#model LoggedOutViewModel
<div class="page-header logged-out">
<small>You are now logged out</small>
...
#if (Model.SignOutIframeUrl != null)
{
<iframe width="0" height="0" class="signout" src="#Model.SignOutIframeUrl"></iframe>
}
</div>
What remains to be done is defining the FrontChannelLogoutUri for your each of your clients. That's normally done in the identity server's config.cs
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
// resource owner password grant client
new Client
{
ClientId = "js",
ClientName = "JavaScript Client",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
RedirectUris = { "http://localhost:5003/callback.html" },
PostLogoutRedirectUris = { "http://localhost:5003/index.html" },
FrontChannelLogoutUri = "http://localhost:5003/frontChannello"
Ok pretty simple. In your Logout action on the account controller (in idserver), make sure you display the LoggedOut view, which in turn shows the iFrame that calls the callback on the mvc client. Pretty much what the spec are saying.
I have an SPA app built with AngularJS, the backend is WebApi2. I´m struggling with Authentication and Authorization. What I want in the long run is to enable authentication against Active Directory. But for now, I just trying to enable authorization for my APiControllers and setting a Cookie with Owin.
Here is my Owin Identity Helper class, I´m only adding 1 claim that is the serialized user info:
public void SignIn(bool rememberMe, T user)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.UserData, JsonConvert.SerializeObject(user)),
};
var claimsIdentity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = rememberMe }, claimsIdentity);
}
Here is authentication in controller:
[HttpGet, HttpPost]
[AllowAnonymous]
[Route("authenticate")]
public HttpResponseMessage Authenticate()
{
var authenticated = IdentityContext.Current.IsAuthenticated;
if (!authenticated)
{
var user = new User();
user.Email = "roger#moore.com";
user.Name = "Roger Moore";
user.Id = 23;
IdentityContext.Current.SignIn(true, user);
return new HttpResponseMessage()
{
Content = new StringContent(
JsonConvert.SerializeObject(user),
Encoding.UTF8,
"application/json"
)
};
}
else
{
//return the user if authenticated
return new HttpResponseMessage()
{
Content = new StringContent(
JsonConvert.SerializeObject(IdentityContext.Current.User), //from claim
Encoding.UTF8,
"application/json"
)
};
}
}
My StartUp class
public partial class Startup
{
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/account/signedout")
});
}
}
When I call the authenticate user I´m setting signing in, but when calling a controller with [Authorize] attribute, im not signed in. Furthermore, when having fiddler running I get the error:
"[Fiddler] Response Header parsing failed. This can be caused by an illegal HTTP response earlier on this reused server socket-- for instance, a HTTP/304 response which illegally contains a body. Response Data:"
Does anyone have any suggestions, or alternatives with example code for using JWT Token Authentication and Authorization From Angular to WebApi2?