DeBiese-Code-Bucket icon indicating copy to clipboard operation
DeBiese-Code-Bucket copied to clipboard

How do you perform logout

Open majhoos opened this issue 7 years ago • 2 comments

Hi Ruben, I posted this question on your blog and Stackoverflow : https://stackoverflow.com/questions/48974995/logout-facility-in-angular-5-application-when-using-windows-authentication

How do you perform logout()

majhoos avatar Feb 25 '18 22:02 majhoos

I'm gonna put my answer to your question on the blogpost here as well. Did you get anywhere with it?

I haven’t given much thought to a logout functionality. I would implement windows authentication only in an intranet environment. Not for a public facing website. And as such, I wouldn’t even want to get the login prompt.

Be that as it may, I searched around a bit, and found something that might be usefull in working towards a solution. Beware: I’m providing some information based on assumptions, I have not tested the following myself. Given the information here: https://en.wikipedia.org/wiki/Basic_access_authentication a possible solution would be to have your server return a 401 with WWW-Authenticate field.

I see 2 options (there can be other and/or better ones):

  1. Keep track of users on your web api (by eg using a custom actionfilterattribute on your controllers/methods). A first time a user makes a request, you return the 401. You can have a caching mechanism to retain the user x-amount of time and/or a logout method in your api.

  2. Implement a logout on your client application. Keep track in your application if the user is in the logged in/logged out status. Send your first request to your web api without addding the credentials to the header. This means that you should find a way for the interceptor not to work on that first request. Easiest way to accomplish this is by starting the app on a login page, clicking the login button sends a request without using the interceptor and you’ll get prompted.

I’m not sure if this helps. If you do try one of those, please let me know how you solved it. I might just add the solution to the blogpost and example code.

DeBiese avatar Apr 06 '18 05:04 DeBiese

I have the following setup which I am not completely happy with but kind of works:

1- Lets say User logs in successfully. Some user information like current user and flags gets stored in localStorage using a UserService:

import { Injectable } from '@angular/core';
import { ReplaySubject } from 'rxjs/ReplaySubject'
import { IUserInfo } from '../../models/IUserInfo';

import { LocalStorageService } from '../../services/local-storage-service';

const userKey = 'user';

@Injectable()
export class UserService {

  status: ReplaySubject<boolean> = new ReplaySubject<boolean>();

  private _hasSignedOut = false;

  constructor(private localStorageService: LocalStorageService) { this.status.next(this.getUser() != null); }

  setUser = (user) => {
    if (user) {
      this.localStorageService.setItem(userKey, user);
      this.status.next(true);
    } else {
      this.localStorageService.removeItem(userKey);
      this.status.next(false);
    }
  }

  get SignOutFlag(): boolean { return this._hasSignedOut; }

  set SignOutFlag(flg: boolean) { this._hasSignedOut = flg; }

  getUser = () => this.localStorageService.getItem(userKey);

  removeUser = () => this.setUser(null);
}

and the LocalStorageService is

import { Injectable } from '@angular/core';

@Injectable()
export class LocalStorageService {

    private cachePrefix = 'cacheData.';

    constructor() { }

    setItem(data, value) {
        const jsonValue = JSON.stringify(value);
        localStorage.setItem(`${this.cachePrefix}${data}`, jsonValue);
    }

    getItem(data) {
        const response = localStorage.getItem(`${this.cachePrefix}${data}`);
        return JSON.parse(response);
    }

    removeItem(data) {
        localStorage.removeItem(`${this.cachePrefix}${data}`);
    }

    clearEntireCache() {
        localStorage.clear();
    }
}

Now a typical Signout >

    public signOut(): void {
        this.userService.removeUser();
        this.localStorageService.clearEntireCache();
        document.execCommand('ClearAuthenticationCache', false);
        this.userService.SignOutFlag = true;
        this.router.navigate(['/login']);
    }

Now, I have a very simple WinLoginComponent:

<div class="win-login-box">
    <a [routerLink]="" (click)="login()">Login</a>
</div>
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router, Params } from '@angular/router';

import { WinAuthService } from '../../services/auth/win-auth.service';
import { UserService } from '../../services/auth/user.service';

@Component({
  selector: 'app-win-login',
  templateUrl: './win-login.component.html',
  styleUrls: ['./win-login.component.css']
})
export class WinLoginComponent implements OnInit {

  constructor(private router: Router, public winAuthService: WinAuthService, public userService: UserService) { }

  ngOnInit() {
  }

  login() {
    console.log('Trying to login back');
    this.winAuthService.loginAfterSignout()
      .map(userData => {
        if (userData) {
          this.userService.setUser(userData);
          this.userService.SignOutFlag = false;
         
        }
      }).subscribe(()=>{
        console.log('I managed to login');
        this.router.navigate(['/dashboard']);
      });
  }
}

Having loginAfterSignout() as:

    loginAfterSignout(): Observable<IUserInfo> {
        const httpClientTarget = `${this._ApiUrl}Auth/GetUser?api-version=1`;

        return this.httpClient.get<IUserInfo>(httpClientTarget, { headers: this.httpClientHeaders, withCredentials: true })
            .do(data => console.log('GetUer returned ' + JSON.stringify(data)))
            .catch(this.handleError);
    }

Also I have a WinAuthInterceptor as follows:

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpInterceptor, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

import { UserService } from '../services/auth/user.service';

@Injectable()
export class WinAuthInterceptor implements HttpInterceptor {
    
    constructor(public userService: UserService) {
     }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        
        if (!this.userService.SignOutFlag){
            req = req.clone({
                withCredentials: true
            });
        }

        return next.handle(req);
    }
}

Notice in above I do not set the credentials flag if the user has signedOut so the user will not be able to access the API unless he logins back. The key problem is that I could not exactly achieve what I wanted without making changes to my Internet Settings

Internet Options > Security Settings > Local Intranet Zone > User Authentication :

By default I had "Automatic logon only in Intranet zone"

Above meant when I try to sign back into the site I get automatically signed backed into the site [without being shown the browser login window]

To be prompted I had to change settings to "Prompt for user name and password" and restart the browser. Changing of the User Authentication/ Logon setting is not something in my control.

Narrowed down version of my problem is: How would I trigger displaying the Windows built in login window on demand without changing my intranet security settings? This is something I could not accomplish in above setup.

majhoos avatar Apr 06 '18 12:04 majhoos