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.
Related
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]")]
I'm running ionic-angular framework working on an app that was started before me. I need to run a function in a service to get an API key from an external server before anything else. Because I want to check if a user has an API key and check if their stored GUID is valid by making another request to the server which I need the API key for. Because I'm going to check if they need to be routed to the login page or not. When I have a route guard checking if a user is logged in my API requests aren't completed or error out because I don't have an API key yet.
If I understood your problem you're asking this: you need to check "something" to decide if the user goes to the login page or goes to another screen, at the beginning of your app, when you open it. And that "something" consists in making http requests.
I will tell you how to do it, but in my experience if you need to make HTTP requests to decide to what screen redirect the user, you will get a blank page in the meanwhile. Depending on the connection 0.1s, 0.3s, 0.7s... But it's uggly. So the alternative would be to create a "Splash" screen with a loader circle, and use that page as the initial page. Then the page checks whatever and takes you to the next page.
Now, answering your question: you need a "CanActivate". CanActivate is guard (a code) that is executed before accessing a route, to check if you can access that page or redirect the user to another page. This is very useful for local checks (like checking the local storage) but as I said, http requests will cause a blank page for a small time. Follow these steps:
1 - First you need to create the CanActivate class. That's like creating a normal Service in ionic. Then make your service implement the CanActivate interface:
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '#angular/router';
import { Observable } form 'rxjs'; // Install 'npm i rxjs' if you don't have it!
#Injectable({
providedIn: 'root'
})
export class LoginGuard implements CanActivate { }
Then this service needs to implement a function called canActivate:
export class LoginGuard implements CanActivate {
constructor(private router: Router) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) : boolean|Observable<boolean> {
return new Observable<boolean>( observer => {
// HERE CHECK
if (!condition_to_avoid_login) {
// Complete the expected route, and enter the login
observer.next(true);
observer.complete();
}
else {
// Avoid login, go somewhere else:
observer.next(false);
this.router.navigate("/my-other-page");
observer.complete();
}
})
}
}
2 - You need to add this Guard to your route. In your routing file: app-routing.module.ts, add this guard to your page:
import { LoginGuard } from '...../login-guard.service';
const routes: Routes = [
...
{
path: 'login',
loadChildren: () => import('...../login.module').then( m => m.LoginPageModule),
canActivate: [LoginGuard]
}
...
]
Now everytime the user accesses this route (/login) the LoginGuard will trigger. There you decide if continue to the login page or redirect.
I have a .NET Core 3.1 web application with React using windows authentication.
When a user enters their Active Directory credentials i would like to verify they belong to a particular Active Directory group before allowing access to the React app.
I have tried setting the default endpoint to a Login Controller to verify the user's groups but i don't know how to redirect to the React app if they do have the valid group.
Startup.cs:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}",
defaults: new { Controller = "Login", action = "Index" });
});
LoginController:
public IActionResult Index()
{
if (HttpContext.User.Identity.IsAuthenticated)
{
string[] domainAndUserName = HttpContext.User.Identity.Name.Split('\\');
//AuthenticateUser verifies if the user is in the correct Active Directory group
if (AuthenticateUser(domainAndUserName[0], domainAndUserName[1]))
{
//This is where i would like to redirect to the React app
return Ok(); //This does not go to the react app
return LocalRedirect("http://localhost:50296/"); //This will keep coming back to this method
}
return BadRequest();
}
}
Is it possible to redirect to the React app from the controller?
Is there a better way to verify an active directory group, possibly through authorizationService.js?
I've been in this situation before, and solved it with custom implementation of IClaimsTransformation. This approach may also be used with OpenId Connect and other authentication systems that requires additional authorization.
With this approach, you can use authorize attribute on controller that serves your React app
[Authorize(Roles = "HasAccessToThisApp")]
and
User.IsInRole("HasAccessToThisApp")
elsewhere in code.
Implementation. Please note that TransformAsync will be called on every request, some caching is recommended if any time-consuming calls.
public class YourClaimsTransformer : IClaimsTransformation
{
private readonly IMemoryCache _cache;
public YourClaimsTransformer(IMemoryCache cache)
{
_cache = cache;
}
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal incomingPrincipal)
{
if (!incomingPrincipal.Identity.IsAuthenticated)
{
return Task.FromResult(incomingPrincipal);
}
var principal = new ClaimsPrincipal();
if (!string.IsNullOrEmpty(incomingPrincipal.Identity.Name)
&& _cache.TryGetValue(incomingPrincipal.Identity.Name, out ClaimsIdentity claimsIdentity))
{
principal.AddIdentity(claimsIdentity);
return Task.FromResult(principal);
}
// verifies that the user is in the correct Active Directory group
var domainAndUserName = incomingPrincipal.Identity.Name?.Split('\\');
if (!(domainAndUserName?.Length > 1 && AuthenticateUser(domainAndUserName[0], domainAndUserName[1])))
{
return Task.FromResult(incomingPrincipal);
}
var newClaimsIdentity = new ClaimsIdentity(
new[]
{
new Claim(ClaimTypes.Role, "HasAccessToThisApp", ClaimValueTypes.String)
// copy other claims from incoming if required
}, "Windows");
_cache.Set(incomingPrincipal.Identity.Name, newClaimsIdentity,
DateTime.Now.AddHours(1));
principal.AddIdentity(newClaimsIdentity);
return Task.FromResult(principal);
}
}
In Startup#ConfigureServices
services.AddSingleton<IClaimsTransformation, YourClaimsTransformer>();
Lately I'm trying to set-up authentication using IdentityServer4 with a React client. I followed the Adding a JavaScript client tutorial (partly) of the IdentityServer documentation: https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf also using the Quickstart7_JavaScriptClient file.
The downside is that I'm using React as my front-end and my knowledge of React is not good enough to implement the same functionality used in the tutorial using React.
Nevertheless, I start reading up and tried to get started with it anyway. My IdentityServer project and API are set-up and seem to be working correctly (also tested with other clients).
I started by adding the oidc-client.js to my Visual Code project. Next I created a page which get's rendered at the start (named it Authentication.js) and this is the place where the Login, Call API and Logout buttons are included. This page (Authentication.js) looks as follows:
import React, { Component } from 'react';
import {login, logout, api, log} from '../../testoidc'
import {Route, Link} from 'react-router';
export default class Authentication extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<div>
<button id="login" onClick={() => {login()}}>Login</button>
<button id="api" onClick={() => {api()}}>Call API</button>
<button id="logout" onClick={() => {logout()}}>Logout</button>
<pre id="results"></pre>
</div>
<div>
<Route exact path="/callback" render={() => {window.location.href="callback.html"}} />
{/* {<Route path='/callback' component={callback}>callback</Route>} */}
</div>
</div>
);
}
}
In the testoidc.js file (which get's imported above) I added all the oidc functions which are used (app.js in the example projects). The route part should make the callback.html available, I have left that file as is (which is probably wrong).
The testoidc.js file contains the functions as follow:
import Oidc from 'oidc-client'
export function log() {
document.getElementById('results').innerText = '';
Array.prototype.forEach.call(arguments, function (msg) {
if (msg instanceof Error) {
msg = "Error: " + msg.message;
}
else if (typeof msg !== 'string') {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById('results').innerHTML += msg + '\r\n';
});
}
var config = {
authority: "http://localhost:5000",
client_id: "js",
redirect_uri: "http://localhost:3000/callback.html",
response_type: "id_token token",
scope:"openid profile api1",
post_logout_redirect_uri : "http://localhost:3000/index.html",
};
var mgr = new Oidc.UserManager(config);
mgr.getUser().then(function (user) {
if (user) {
log("User logged in", user.profile);
}
else {
log("User not logged in");
}
});
export function login() {
mgr.signinRedirect();
}
export function api() {
mgr.getUser().then(function (user) {
var url = "http://localhost:5001/identity";
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = function () {
log(xhr.status, JSON.parse(xhr.responseText));
}
xhr.setRequestHeader("Authorization", "Bearer " + user.access_token);
xhr.send();
});
}
export function logout() {
mgr.signoutRedirect();
}
There are multiple things going wrong. When I click the login button, I get redirected to the login page of the identityServer (which is good). When I log in with valid credentials I'm getting redirected to my React app: http://localhost:3000/callback.html#id_token=Token
This client in the Identity project is defined as follows:
new Client
{
ClientId = "js",
ClientName = "JavaScript Client",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
// where to redirect to after login
RedirectUris = { "http://localhost:3000/callback.html" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:3000/index.html" },
AllowedCorsOrigins = { "http://localhost:3000" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
Though, it seems the callback function is never called, it just stays on the callback url with a very long token behind it..
Also the getUser function keeps displaying 'User not logged in' after logging in and the Call API button keeps saying that there is no token. So obviously things are not working correctly. I just don't know on which points it goes wrong.
When inspecting I can see there is a token generated in the local storage:
Also when I click the logout button, I get redirected to the logout page of the Identity Host, but when I click logout there I don't get redirected to my client.
My questions are:
Am I on the right track implementing the oidc-client in combination with IdentityServer4?
Am I using the correct libraries or does react require different libraries than the oidc-client.js one.
Is there any tutorial where a react front-end is used in combination with IdentityServer4 and the oidc-client (without redux), I couldn't find any.
How / where to add the callback.html, should it be rewritten?
Could someone point me in the right direction, there are most likely more things going wrong here but at the moment I am just stuck in where to even begin.
IdentityServer4 is just a backend implementation of OIDC; so, all you need to do is implement the flow in the client using the given APIs. I don't know what oidc-client.js file is but it is most likely doing the same thing that you could have implemented yourself. The flow itself is very simple:
React app prepares the request and redirects the user to the Auth server with client_id and redirect_uri (and state, nonce)
IdentityServer checks if the client_id and redirect_uri match.
If the user is not logged in, show a login box
If a consent form is necessary (similar to when you login via Facebook/Google in some apps), show the necessary interactions
If user is authenticated and authorized, redirect the page to the redirect_uri with new parameters. In your case, you the URL will look like this: https://example.com/cb#access_token=...&id_token=...&stuff-like-nonce-and-state
Now, the React app needs to parse the URL, access the values, and store the token somewhere to be used in future requests:
Easiest way to achieve the logic is to first set a route in the router that resolves into a component that will do the logic. This component can be "invisible." It doesn't even need to render anything. You can set the route like this:
<Route path="/cb" component={AuthorizeCallback} />
Then, implement OIDC client logic in AuthorizeCallback component. In the component, you just need to parse the URL. You can use location.hash to access #access_token=...&id_token=...&stuff-like-nonce-and-state part of the URL. You can use URLSearchParams or a 3rd party library like qs. Then, just store the value in somewhere (sessionStorage, localStorage, and if possible, cookies). Anything else you do is just implementation details. For example, in one of my apps, in order to remember the active page that user was on in the app, I store the value in sessionStorage and then use the value from that storage in AuthorizeCallback to redirect the user to the proper page. So, Auth server redirects to "/cb" that resolves to AuthorizeCallback and this component redirects to the desired location (or "/" if no location was set) based on where the user is.
Also, remember that if the Authorization server's session cookie is not expired, you will not need to relogin if the token is expired or deleted. This is useful if the token is expired but it can be problematic when you log out. That's why when you log out, you need to send a request to Authorization server to delete / expire the token immediately before deleting the token from your storage.
I am facing issue in navigating to URL. My default page set to Login and my application URL is http://localhost:12345/#/.
This works well but there are two ways the user can login to application
Direct through the application.
Getting username and password trough query string.
When application is logging through Query String the url is like http://localhost:12345?auth=123654654656564/#/.
I would like to remove auth value from the URL. I tried to map the routing but it doesn't work.
routes.MapRoute(
name: "Default",
url: "{controller}/{action}",
defaults: new { controller = "Account", action = "Login"}
);
And also i tried to create one more action result that will return the view
routes.MapRoute(
name: "ActualDefault",
url: "{controller}/{action}",
defaults: new { controller = "Account", action = "LoginQuery" }
);
Controller:
public ActionResult Login()
{
if (Request.QueryString.Count > 0 && Request.QueryString != null)
{
//validating
return RedirectToAction("LoginQuery", "Account");
}
else
{
return View();
}
}
public ActionResult LoginQuery()
{
return View("Index");
}
The above code removes query string but the URL will be http://localhost:12345/Account/LoginQuery/#/.
I just need the URL like http://localhost:12345/#/.
Logging in via Query String
I would be negligent not to point out that this is an extremely bad practice. You should always use HTTP POST when logging into an application and send the user secrets in the body of the post, not the query string.
See
Handling Form Edit and Post Scenarios
Submit Form with Parameters in ASP.NET MVC
BUILDING ASP.NET MVC FORMS WITH RAZOR
Note that you can also create forms in plain HTML (or via angularjs) to call an MVC action method, or you can make an HTTP POST via JavaScript or some other programming language to do the same thing.
Query string values are completely ignored by MVC routing. But you can make a custom route use query string values.
public class LoginViaQueryStringRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var path = httpContext.Request.Path;
if (!string.IsNullOrEmpty(path))
{
// Don't handle URLs that have a path /controller/action
return null;
}
var queryString = httpContext.Request.QueryString;
if (!queryString.HasKeys())
{
// Don't handle the route if there is no query string.
return null;
}
if (!queryString.AllKeys.Contains("username") && !queryString.AllKeys.Contains("password"))
{
// Don't handle the case where controller and action are missing.
return null;
}
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values["controller"] = "Account";
routeData.Values["action"] = "LoginQuery";
routeData.Values["username"] = queryString["username"];
routeData.Values["password"] = queryString["password"];
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
Usage
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new LoginViaQueryStringRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
This route will now match http://localhost:12345/?username=foo&password=bar and send it to your LoginQuery action method.
Logging in via http://localhost:12345/#/
It is unclear how you expect this to work. Since everything after the hash tag are generally not sent to the server from the browser, http://localhost:12345/#/ is equivalent to http://localhost:12345/. So, you are effectively saying "I want my home page to be the login page".
In a typical MVC application, you would setup an AuthorizeAttribute on the home page to redirect the user to the login page. After the user logs in, they would be redirected back to the home page (or usually whatever secured page they initially requested).
[Authorize]
public ActionResult Index()
{
return View();
}
If you want all of your application secured, you can register the AuthorizeAttribute globally and use AllowAnonymousAttribute on your public action methods (such as the login and register pages).
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new AuthorizeAttribute());
filters.Add(new HandleErrorAttribute());
}
}
And your login action methods:
[AllowAnonymous]
public ActionResult Login()
{
//...
}
[AllowAnonymous]
[HttpPost]
public ActionResult Login(LoginModel model)
{
//...
}
[AllowAnonymous]
public ActionResult LoginQuery(string username, string password)
{
//...
}
But then, that is a typical MVC-only application.
If you are using Angular to make a SPA, then this could be a very different story. Namely, you would probably switch views on the client side without doing an HTTP 302 redirect to the login form (perhaps it would be a popup - who knows). The point is, without any details of how the client is setup to communicate with MVC, it is not possible to give you any useful advice on setting up MVC for your client beyond how you would typically setup MVC to work in a multi-page application.
NOTE: I can tell you that your routing is misconfigured. The Default and ActualDefault definitions cannot exist in the same route configuration because the first match always wins, therefore the first one will run and the other one will never run. Both of the route URL definitions will match any URL that is 0, 1, or 2 segments in length, so whichever you have first in the route table will match and the other one will be an unreachable execution path.