I recently enabled Angular Universal in my project. Everything works as expected, except for one strange issue. Whenever I refresh page or click a link in email that navigates to webpage, I see Login page for brief second then actual page loads.
Sample project created and uploaded to Github. Remember the delay may not as long as in my real project.
Github repo link: https://github.com/pavankjadda/angular-ssr-docker
Problem:
As it turns out when Ng Express Engine loads the web page, it does not have access to cookies. Hence it redirects user to Login page, but as soon as browser loads JavaScript (Angular), which checks for cookies and validates Authentication guards, redirects user to actual webpage. The ideal solution would be making cookies available on server side (sending it through request) and making sure Authentication guards passes. I tried send the cookies through server.ts, but couldn't get it working.
Work Around:
Until I figure out the solution here is the work around I followed. Whenever we check for cookies, determine if the platform is server, if yes return true. Here are the few places where you can make this change
Make sure authservice.ts returns true when the platform is server
/**
* Returns true if the 'isLoggedIn' cookie is 'true', otherwise returns false
*
* #author Pavan Kumar Jadda
* #since 1.0.0
*/
isUserLoggedIn(): boolean {
return isPlatformServer(this.platformId) || (this.cookieService.get('isLoggedIn') === 'true' && this.cookieService.check('X-Auth-Token'));
}
Do the same thing in Authentication guards
#Injectable({
providedIn: 'root'
})
export class CoreUserAuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router, #Inject(PLATFORM_ID) private platformId: any,) {
}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const url: string = state.url;
return this.checkLogin(url);
}
/**
* Returns true if the user is Logged In and has Core user role
*
* #author Pavan Kumar Jadda
* #since 1.0.0
*/
private checkLogin(url: string): boolean {
// Return if the platform is server
if (isPlatformServer(this.platformId))
return true;
if (this.authService.isUserLoggedIn() && this.authService.hasCoreUserRole()) {
return true;
}
if (this.authService.isUserLoggedIn() && !this.authService.hasCoreUserRole()) {
this.router.navigate(['/unauthorized']);
}
// Store the attempted URL for redirecting
this.authService.redirectUrl = url;
// Navigate to the login page with extras
this.router.navigate(['/login']);
return false;
}
}
Note: Added work around here, in case if anyone has similar problem. When I have an actual solution to the problem, I will update this answer.
change your isUserLoggedIn() function in auth service to:
public async isUserLoggedIn(): Promise<boolean> {
const isLoggedIn = await this.cookieService.check("token");
return isLoggedIn;
}
Related
can anyone let know how can we validate the token stored in local storage for routing protection
I have saw some of tutorials but all of them checking if there is any token is present in local storage or not like below4
export class AuthGuard implements CanActivate {
constructor(private routes: Router) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable < boolean > | Promise < boolean > | boolean {
if (localStorage.getItem('token') != null) {
return true;
} else {
this.routes.navigate(['/login']);
return false;
}
}
}
and we can easily bypass this method by creating a token with random value
can anyone let me know more efficient way to validate the login token?
token validation in angular authguard
As rightly said by Heiko & Jimmy, its not the secure way to validate token at the client side.
However, assuming that you want to implement it at client side only without any server validation.
To do that in the client side
This is the best article I suggest
https://www.syncfusion.com/blogs/post/best-practices-for-jwt-authentication-in-angular-apps.aspx
So I have a basic Setup. IdentityServer with 3 login options. Username/Password, Google login and an External Login provider. As it is right now when I run the following code:
var config = {
authority: "https://example.com",
client_id: "js",
redirect_uri: "https://example.com/callback.html",
response_type: "code",
scope: "openid profile api1",
post_logout_redirect_uri: "https://example.com/index.html",
};
var mgr = new Oidc.UserManager(config);
function login() {
mgr.signinRedirect();
}
I get redirected to the following page.
When I press login I get redirected to my login page:
This is all fine. But now I have a new requirement for the login process. They want a specific login button on the javascript client to start login with google and a specific button to login with OpenIDConnect login provider from the javascript Client.
So I could have some subpage eg. /GoogleIntegrations.html or whatever. And there I would like to have a button "Try google login", that should start loginflow with google, by redirect to the IdentityServer and then make the google redirect instantly afterwards.
Has anyone had similar usecase?
Edit1:
Account controller can be found here:
https://github.com/TopSwagCode/Dotnet.IdentityServer/blob/master/src/IdentityServerAspNetIdentity/Controllers/Account/AccountController.cs
Edit2:
I have tried the solution by abdusco, but I am stuck on the identity server. I don't get redirected back to my Javascript client.
eg: If I have button with:
https://localhost:5001/External/Challenge?scheme=OpenIdConnect
I do get redirected to the external login provider. But when I login I am stuck on the Identity Server page.
I have also tried to login with the following link:
https://localhost:5001/External/Challenge?returnurl=/connect/authorize/callback?client_id=js&redirect_uri=https%3A%2F%2Flocalhost%3A5003%2Fcallback.html&response_type=code&scope=openid%20profile%20api1&scheme=OpenIdConnect
GIF of how it looks with normal flow:
Edit3:
using the following code from comment:
var returnUrl = location.href;
var url = "https://localhost:5001/External/Challenge?returnurl=" + returnUrl + "&scheme=OpenIdConnect"
location.href = url;
throws exception. See code screenshot below
Edit4:
I was looking into another aproach, but still not entirely working as I want. On the login page on Identity, you can bypass username/password login. If I comment out that code and just redirect to my OpenIdConnect login. I get login and redirect back to client as I want, but this will then not allow me to use google or username password login.
Code looks like the following:
/// <summary>
/// Entry point into the login workflow
/// </summary>
[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
// build a model so we know what to show on the login page
var vm = await BuildLoginViewModelAsync(returnUrl);
var context = HttpContext;
//if (vm.IsExternalLoginOnly)
//{
// we only have one option for logging in and it's an external provider
return RedirectToAction("Challenge", "External",
new { scheme = "OpenIdConnect", provider = vm.ExternalLoginScheme, returnUrl });
//}
return View(vm);
}
I found a solution myself.
So first off I created a new Callback URL for the OpenIDConnect Users. Eg. /admincallback.html
Then I can use the OIDC Usermanger javascript like so:
mgr.signinRedirect({ redirect_uri: 'https://localhost:5003/admincallback.html' });
While normal login button just use the default /callback.html
This will let me check on the Identity backend what callback is being used and based on that what login page to show. Default or instant challenge external providers. Like so:
/// <summary>
/// Entry point into the login workflow
/// </summary>
[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
// build a model so we know what to show on the login page
var vm = await BuildLoginViewModelAsync(returnUrl);
if (returnUrl.Contains("admincallback"))
{
// we only have one option for logging in and it's an external provider
return RedirectToAction("Challenge", "External",
new { scheme = "OpenIdConnect", provider = vm.ExternalLoginScheme, returnUrl });
}
return View(vm);
}
You could create your callback url's to contain provider name like: /callback/google, /callback/oidc, /callback/azure, etc. This would let you dynamically decide which button was pressed. Instead of having a bunch of if statements for each supported provider.
This approach requires you to remember to add these new callback url's to the database. All code can be found here:
https://github.com/TopSwagCode/Dotnet.IdentityServer
This is just a project where I share all IdentityServer related stuff I learn.
As you already have ExternalController.Challenge method in your app,
create a URL that specifies the scheme, and you won't have to change anything.
/// <summary>
/// initiate roundtrip to external authentication provider
/// </summary>
[HttpGet]
public IActionResult Challenge(string scheme, string returnUrl)
{
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
// validate returnUrl - either it is a valid OIDC URL or back to a local page
if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false)
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
// start challenge and roundtrip the return URL and scheme
var props = new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", scheme },
}
};
return Challenge(props, scheme);
}
login with google
Create an endpoint in your app, and issue a challenge for Google auth.
public class AccountController : ControllerBase
{
[AllowAnonymous]
[HttpGet("/login-google")]
public ActionResult LoginGoogle()
{
return Challenge(
new AuthenticationProperties
{
RedirectUri = Url.Action("ExternalCallback"),
// RedirectUri = Url.Page("LoginCallback"),
}, GoogleDefaults.AuthenticationScheme
);
}
}
Add a button, when clicked, redirect the user to this endpoint.
User will be forced to sign in to his Google account directly without showing the login view of your app.
<button onclick="location.href='/login-google'">login with google</button>
<!-- or a simple anchor should suffice -->
login with google
Note that this the most basic setup. You must have an AccountController or Login razor page that contains the logic for what to do when the user returns from Google, so try setting the redirect URL to that action.
I'm working in an Angular 10 project, I am also using firebase hosting and cloud firestore (for DB). I am using AngularFire in my project as well.
My project is already able to get documents from my firestore collection, and display them (also can edit, delete, and create them). I also set up authentication, where I use AngularFireAuth to sign in and sign out. I also have route guards to only allow users access to info after signing in.
I've discovered that Firestore also has rules, and that you should set them up to secure your collection. Currently, I basically have no rules (test mode), but I want to add a basic "only users can access anything" rule, but am running into an issue.
I think this is the issue, currently, after logging in my app will store the user in local storage. I think that I need to store this a different way so I am re-signed in from previously given creds instead of just checking if there local storage. I only get ERROR FirebaseError: Missing or insufficient permissions errors when my guard checks the local storage to ensure sign-in, if I sign-in first, I don't get the error.
So, how should I save user data so that I don't have to sign-in on every refresh, but that I can verify the auth to firebase? I know I could store the email/password to local storage and check to re sign-in, but that seems insecure to me.
I think the above is the issue, but not 100% sure.
This is my firestore rule:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
allow read, write: if request.auth != null
match /{document=**} {
allow read, write: if request.auth != null //should only allow users?
}
}
}
Here is my auth service (where I handle sign-in/sign-out and check if local storage has user.
export class AuthService {
constructor(private aFAuth: AngularFireAuth, public router: Router) {
//I honestly don't know if I need this
this.aFAuth.authState.subscribe((user) => {
if (user) {
localStorage.setItem('my-test-app-currentUser', JSON.stringify(user));
} else {
localStorage.setItem('my-test-app-currentUser', null);
}
});
}
async signIn(email: string, password: string) {
this.aFAuth
.signInWithEmailAndPassword(email, password).then((result) => {
localStorage.setItem('my-test-app-currentUser', JSON.stringify(result.user));
this.router.navigate(['']);
}).catch((error) => {
window.alert(error.message);
});
}
//this is the func that needs to change, if I have storage, I need to be able to sign-in with it again
isSignedIn(): boolean {
if (!localStorage.getItem('my-test-app-currentUser')) {
return false;
}
return true;
}
signOut() {
return this.aFAuth.signOut().then(() => {
localStorage.removeItem('my-test-app-currentUser');
window.alert('You have been signed-out');
});
}
}
Here my guard:
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
// return true;
if (this.auth.isSignedIn()) {
return true;
} else {
this.router.navigate(["sign-in"]);
return false;
}
}
Any help would be appreciated.
Firebase already stores the user credentials in local storage, and automatically restores them when you reload the page.
Restoring them does require a check against the server though, so it happens asynchronously. For that reason, any code that depends on the user's authentication state should be inside the this.aFAuth.authState.subscribe handler, so that it runs whenever the authentication state changes.
So instead of handling the navigation when the signInWithEmailAndPassword call completes, which happens only when you actively sign the user in, the navigation should be in the auth listener, which runs both on active sign in and on a restore.
So something like:
export class AuthService {
constructor(private aFAuth: AngularFireAuth, public router: Router) {
//I honestly don't know if I need this
this.aFAuth.authState.subscribe((user) => {
if (user) {
localStorage.setItem('my-test-app-currentUser', JSON.stringify(user));
this.router.navigate(['']);
} else {
localStorage.setItem('my-test-app-currentUser', null);
}
});
}
async signIn(email: string, password: string) {
this.aFAuth
.signInWithEmailAndPassword(email, password).then((result) => {
window.alert(error.message);
});
}
...
In your canActivate you'll probably want to use the AngularFireAuthGuard. which ensures that unauthenticated users are not permitted to navigate to protected routes. I think this might replace your entire need for local storage.
Also see the AngularFire documentation on Getting started with Firebase Authentication and Route users with AngularFire guards.
I would like to create a route guard for protecting routes against unauthorized users.
I am using jsonwebtoken for authorization, and at the moment storing that in localStorage.
My idea is, when a user wants to access a protected admin route, authguard sends the token for validation to the nodeJS/Express server that after validation returns a true or 401 (whether the user is admin) to the client side.
auth service:
isLoggedIn(){
let headers = new HttpHeaders().set('x-auth-token',localStorage.getItem('token') || '');
return this.http.post('http://localhost:3000/api/users/check-auth', {}, { headers: headers }).toPromise();
}
authGuard service:
canActivate(){
return this.sign.isLoggedIn().then(res => {return res;}).catch(ex => {return ex});
}
My purpose would be to avoid manually setting a token key in the localstorage by the user to see the guarded route, even if he would not be able to implement any XHR request.
Could you please verify if its a good or bad idea and come up with better solution on security side?
Many thanks!
A good practice would be to manage roles (or permissions) at the model level on the server-side. For example a User class could have a roles property, such as :
auth.service.ts
myUser.roles = ['ROLE_ADMIN']
This way, when your user logins, you can store the information in your auth.service.ts
// auth.service.ts
get isAdmin() {
return this.user.roles.includes('ROLE_ADMIN')
}
Note that usually you want to store this information in you app state management, whether it be plain rxjs, ngrx, ngxs...
Finally you would add an AuthInterceptor which would redirect your user if your API returns a 401.
I am currently developing a web-application using Angular2 with TypeScript (if that matters).
This application has to communicate with a webserver, which is asking for a digest authentication.
Until now i was using the native login prompt of the browsers, which is automatically showing, when the server returns a "401 unauthorized". The browser only asks for authentication once and automatically uses this username and password for future requests. So I don't have to take care about the authentication, the browser does everything for me.
Unfortunately now i have to create a custom login screen, as i have to implement some default actions, such as "register" or "reset passwort", which are ususally accessible from that screen.
As digest authentication is quite complex and the browser would allready do all the complex things for me I would like to continue using the browsers functionality, but without using it's login prompt.
So is it possible to use the browsers authentication functionality?
If it is possible, how can I set the username and the password it should use?
EDIT:
As someone wanted to close this question as "to broad", i'll try to add some more detail.
The web-application gets data from a restful webservice. This webservice requires digest authentication and responds with a 401, if you are using a wrong username or password.
As mentioned above, the browser automatically shows a login prompt, if he gets a 401 error. If you enter a valid login, the browser caches those values somewhere and automatically sets them for every future request.
Now i basicly want to replace the login prompt and programatically set the values the browser should use for the login.
I hope this helps to make the question clear.
Basically you have to write a HTTP decorator to intercept response code 401. Afterwards you add the Authentication header and replay the request.
import { Injectable } from '#angular/core';
import { Http, ConnectionBackend, RequestOptions, RequestOptionsArgs, Response, Headers } from '#angular/http';
import { Observable } from 'rxjs/Rx';
#Injectable()
export class CustomHttp extends Http {
/**
* intercept request
* #param {Observable<Response>} observable to use
* #param {string} url to request
* #returns {Observable<Response>} return value
* #private
*/
private _intercept(observable: Observable<Response>, url: string): Observable<Response> {
return observable.catch(error => {
if (error.status === 401) {
// add custom header
let headers = new Headers();
headers.append('Authentication', '<HEADER>');
// replay request with modified header
return this.get(url, new RequestOptions({
headers: headers
});
} else {
return Observable.throw(error);
}
});
};
/**
* constructor
* #param {ConnectionBackend} backend to use
* #param {RequestOptions} defaultOptions to use
* #returns {void} nothing
*/
constructor(backend: ConnectionBackend, defaultOptions: RequestOptions) {
super(backend, defaultOptions);
};
/**
* get request
* #param {string} url to request
* #param {RequestOptionsArgs} options to use
* #returns {Observable<Response>} return value
*/
get(url: string, options?: RequestOptionsArgs): Observable<Response> {
return this._intercept(super.get(url, options), url);
};
}
What didn’t worked for me yet is determining correct responses as there was a bug filed against Angular2 (https://github.com/angular/angular/pull/9355), which was merged just a few days before.
You have to increment the request counter for each valid request following the first successful one.
Maybe somebody else can show up with a working solution.