Authentication Bearer

It’s a new day and I’m back at home. I’m still a bit focused on the slow progress of the error log application. Although I can call out all of the progress being made, it still feels slow. I want to get to the development of the actual game. All of this groundwork is good for laying the infrastructure, but doesn’t feel like it’s getting me any closer to publishing the game. At least the good news is that it’s generic enough that it’s good for any platform moving forward. There isn’t anything specific to the game itself that’s been developed yet other than experimenting with WebGL’s 3D Modeling, scanning QR codes with the Barcode API, and digital communications with the Web Audio API. Still, those are simply experiments and not the actual building blocks of the game.

Ideas are cheap and abundant… Peter Drucker

Ideas are cheap and abundant; what is of value is the effective placement of those ideas into situations that develop into action.

Peter Drucker
Austrian American management consultant, educator, and author

Yesterday we left off having learned about creating forms and custom validators to allow the end-user to edit their credentials and setup two-factor authentication with a URL provisioned with a QR code. Today we get to restrict access to require authentication.

Login Form Prompt: Microsoft Image Creator

I created a login form and 2FA form, adding access controls to sensitive information within error logs. Endpoints were reconfigured to require a valid authorization header before allowing data to be returned.

Well… I feel silly. I just learned how to generate components with the command line. Here I was copying and pasting component files. Not only that, it created a separate folder next to all of my other component folders. It looks like my intuition of rearranging the default project structure was correct.

âžś  errors.periplux.io git:(production) ng generate component login
CREATE src/app/login/login.component.scss (0 bytes)
CREATE src/app/login/login.component.html (20 bytes)
CREATE src/app/login/login.component.spec.ts (585 bytes)
CREATE src/app/login/login.component.ts (231 bytes)
âžś  errors.periplux.io git:(production) âś— 

Well… I’m a bit stuck. I created my login component. I added a new route to see my component, but I keep getting redirected to the error logs page.

import { Routes } from '@angular/router';
import { LogComponent } from './app/log/log.component';
import { LoginComponent } from './app/login/login.component';
export const routes: Routes = [
  { path: '', component: LogComponent },
  { path: 'login', component: LoginComponent }
];

So going to /login sends me to /.

I tried adding forward slashes at the beginning of the paths, but Angular complains about it. I added pathMatch: 'full' to the root path, but it’s still taking over the login path. Removing the empty path entry – I’m still seeing the error logs. I’m definitely overlooking something. Maybe routes are not being applied at all.

I got somewhere. My app.component.html had <app-logs></app-logs>. This meant that whatever the url was, the app was always rendering the logs component. I changed it to be <router-outlet></router-outlet> and my login form appeared.

Awesome. Our login form displays without any error. I copied most of the logic and html from the credentials editor yesterday.

We’ve got our login form, but now we have lost our logs. Going to the root url, I just see a blank page with an error in the console.

NullInjectorError: NullInjectorError: No provider for InjectionToken MatMdcDialogData!
    at NullInjector.get (core.mjs:1660:27)
    at R3Injector.get (core.mjs:3106:33)
    at R3Injector.get (core.mjs:3106:33)
    at R3Injector.get (core.mjs:3106:33)
    at ChainedInjector.get (core.mjs:5446:36)
    at lookupTokenUsingModuleInjector (core.mjs:5799:39)
    at getOrCreateInjectable (core.mjs:5847:12)
    at Module.ɵɵdirectiveInject (core.mjs:11383:19)
    at NodeInjectorFactory.LogComponent_Factory [as factory] (log.component.ts:49:26)
    at getNodeInjectable (core.mjs:6059:44)

The LogComponent is having a problem injecting the material dialog data. Why Log? It should be Logs that loads. Ahah! My bad. I referenced the wrong component in the routes file. I now have a grid… but it’s empty?

First, let’s rename out Log and Logs components to be easier to discern. Especially now that we are adding Login. Log, Logs, Login… I renamed the logs components to logList, logItem, logItemDetails, and logItemDates. That should be distinct enough to keep them separate.

Why is my list empty? From what I see, a call isn’t being made to grab data when the page loads. If I search for anything, a request is made. Let’s review our initializer.

After I had wired up the search results to work with query string parameters the other day, I subscribed to the router and loaded the page once I saw a navigation end event. The problem now is that I’m not seeing any router events after the component is initialized until I update the query string parameters.

Why did I subscribe to the router? The problem was that although I specified query string parameters in the URL, the activatedRoute was empty. Reflecting back on this, it was probably because I didn’t setup routing correctly and that the component wasn’t actually being loaded by the router. I’ve plugged in some logs to monitor the lifecycle when the page loads. Let’s see if our Activated Route has parameters now that we changed things around…

Testing lifecycle with activated route and router events
ngOnInit() {
    console.log('ngOnInt');
    this.activatedRoute.queryParams.subscribe(params => {
      console.log('activatedRoute', params);
    })
    this.router.events.subscribe(event => {
      console.log('router event');
      if (event instanceof NavigationEnd) {
        console.log('...navigationEnd');
        const page = this.activatedRoute.snapshot.queryParamMap.get('page');
        const size = this.activatedRoute.snapshot.queryParamMap.get('size');
        const id = this.activatedRoute.snapshot.queryParamMap.get('id');
        const searchText = this.activatedRoute.snapshot.queryParamMap.get('search') ?? '';

Holy smokes. We’ve got our parameters! The router event subscription is no longer needed. After reworking the logic to subscribe to the activated route, the logs list loaded up just fine.

Initializing with activated route
ngOnInit() {
    this.activatedRoute.queryParams.subscribe(params => {
      const { page = '', size = '', id = '', searchText = '' } = params;
      let pageIndex = page ? parseInt(page, 10) - 1 : 0;
      if (pageIndex < 0) pageIndex = 0;
      let pageSize = size ? parseInt(size, 10) : defaultPageSize;
      if (!this.pageSizeOptions.includes(pageSize)) {
        pageSize = defaultPageSize;
      }
      let logId = id ? parseInt(id, 10) : undefined;
      this.selectedId = logId;
      this.searchInput = searchText;
      this.loadData(pageIndex, pageSize, searchText);
    });
  }

So where were we? Yes – login. Our log list works and our login page works. We need to prevent access to the log list if the user hasn’t authenticated. Ideally, we also need to be able to let the user continue to their original destination once they login. If I follow a link to open log item #54, the item should be displayed after authenticating.

Routes have a flag, canActivate that can be passed a list of guard classes implementing the CanActivate interfaces.

Guarding Routes
// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { DatePipe } from '@angular/common';
import { provideAnimations } from '@angular/platform-browser/animations';
import { AgePipe } from './app/pipes/AgePipe';
import { DurationPipe } from './app/pipes/DurationPipe';
import { routes } from './app.routes';
import { AuthGuard } from './AuthGuard';
import { AuthService } from './AuthService';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient(),
    provideAnimations(),
    AuthGuard,
    [{ provide: AuthService, useClass: AuthService, providedIn: 'root' }],
    DatePipe,
    AgePipe,
    DurationPipe
  ]
};
//app.routes
import { Routes } from '@angular/router';
import { LogListComponent } from './app/logList/logList.component';
import { LoginComponent } from './app/login/login.component';
import { AuthGuard } from './AuthGuard';
export const routes: Routes = [
  { path: '', component: LogListComponent, pathMatch: 'full', canActivate: [AuthGuard] },
  { path: 'login', component: LoginComponent }
];
// AuthGuard.ts
// returns false and redirects to /login if not authenticated
// otherwise returns true
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import {
  CanActivate,
  Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  UrlTree
} from '@angular/router';
import { AuthService } from './AuthService';

type activateResponse = boolean |
  UrlTree |
  Observable<boolean | UrlTree> |
  Promise<boolean | UrlTree>;

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor(
    private router: Router,
    private authService: AuthService
  ) { }
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): activateResponse {
    if (this.authService.isLoggedIn()) {
      return true;
    } else {
      const extras = {
        queryParams: {
          returnUrl: state.url
        }
      };
      this.router.navigate(['/login'], extras);
      return false;
    }
  }
}
// AuthService.ts
export class AuthService {
  authenticated: boolean = false;
  isLoggedIn() {
    return this.authenticated;
  }
  setIsLoggedIn(value: boolean) {
    this.authenticated = value;
  }
}

Basically I created an authentication service that maintained a state if the user was logged in or not. The authentication guard would call the service to verify if the user was logged in and either let them pass, or redirect them to the login page and add a query string parameter indicating where the user should return. Once the user logs in with the login component, the login component calls the authentication service to let it know that the user is authenticated. Afterwards, it redirects the user back to the original url that they requested.

To see it in action, I also had to create an endpoint in PHP to reveal the credentials secret and hash the password to determine if there is a match.

PHP endpoint to login with hashed password
<?php
require_once "../common/Show.php";
require_once "../common/Secrets.php";
require_once "../common/PostedJson.php";

function main()
{
    $posted = new PostedJson(2);

    if (!$posted->keysExist('username', 'password')) {
        Show::error($posted->lastError(), $posted->lastErrorCode());
        exit;
    }

    $username = $posted->getValue('username');
    $password = $posted->getValue('password');

    $username = trim($username);
    $password = trim($password);

    if ($username === '') {
        Show::error("Invalid username");
        exit;
    }
    if ($password === '') {
        Show::error("Invalid password");
        exit;
    }

    $credentials = Secrets::revealAs("CREDENTIALS", 'array');

    if ($credentials['username'] !== $username) {
        Show::error('Invalid credentials');
        exit;
    }

    $password_salt = $credentials['password_salt'];
    $hash = hash('sha256', $password . $password_salt);
    $password_hash = bin2hex($hash);

    if ($credentials['password_hash'] !== $password_hash) {
        Show::error('Invalid credentials');
        exit;
    }

    Show::message('Login successful');
    exit;
}
main();

Along the way I ran into a few errors in the PHP script and thought I would have to disable the guard just to view the error. Lucky for me, the actual error log in production was still unprotected and I could see the error.

We now have authentication. Yay! Now what? Refreshing the page logs us out. Before we address that, we need to implement Two-factor authentication. It looks like we can add an additional guard specifically for this purpose.

Our two-factor authentication guard needs to make a request to the web server to determine if 2FA is enabled. If not, the user can go through.

I’ve made a bit of progress. I did a bit of fancy footwork to determine if tfa is enabled by storing a tri-state/undefinable boolean. If the value is false, return true. If the value is true, return true if they have been authenticated; otherwise return false. If undefined, we need to make a request to determine if the user needs 2FA authentication. I piped the response into a boolean. From there, the TfaGuard was able to subscribe and redirect the user

Memory: TFA EnabledAPI: TFA EnabledMemory: TFA AuthenticatedCan Activate
NoYes
YesNoNo
YesYesYes
?NoYes
?YesNoNo
?YesYesYes

In addition to this, I put a guard onto the TFA route so that you can’t access the page until you’ve authenticated with a username and password. I also created two PHP endpoints to both check if TFA is enabled, and to verify the OTP provided.

PHP Two-Factor Authentication – Enabled?
<?php
require_once "../common/Show.php";
require_once "../common/Secrets.php";
function main()
{
    $secret = Secrets::reveal("OTP_SECRET");
    if ($secret === false || $secret === '') {
        Show::data(['enabled' => false]);
        exit;
    }
    Show::data(['enabled' => true]);
    exit;
}
main();
PHP Two-Factor Authentication – Verify OTP
<?php
require_once "../common/Show.php";
require_once "../common/Secrets.php";
require_once "../common/PostedJson.php";
require_once "../common/Otp.php";

function main()
{
    $posted = new PostedJson(2);

    if (!$posted->keysExist('otp')) {
        Show::error($posted->lastError(), $posted->lastErrorCode());
        exit;
    }

    $otp = $posted->getValue('otp');

    if ($otp === '') {
        Show::error("Invalid otp");
        exit;
    }

    $secret = Secrets::reveal("OTP_SECRET");
    if ($secret === false || $secret === '') {
        Show::data(['verified' => true]);
        exit;
    }

    $otpManager = new Otp($secret);
    if (!(
        $otp === $otpManager->otp()
        || $otp === $otpManager->get_relative_otp(-1)
        || $otp === $otpManager->get_relative_otp(1)
    )) {
        Show::error("Incorrect OTP provided.");
        return;
    }

    Show::data(['verified' => true]);
    exit;
}
main();

I’m kinda stuck in an odd issue with the returnUrl. Once I authenticate, I have problems loading the correct page.

From the above example, it looks like I attempted to log into the root page of /, got past the login form, and was viewing the 2FA form – at which time I made a code change, triggering the 2FA page the reload – sending me back to the login form. After providing my login credentials again, it bombs out. It isn’t able to discern where to send me as /tfa?returnUrl=%2F does not match the tfa route.

Normally, the query string parameters are passed to the routers navigate method as an extras object in the guards.

const extras = {
  queryParams: {
    returnUrl: state.url
  }
};
this.router.navigate(['/tfa'], extras);

That looks fine… the problem is when the return url itself contains query string parameters when navigating after logging in. The router tries to match the query string parameters as part of the url.

if (this.returnUrl) {
  this.router.navigate([this.returnUrl]);
} else {
  this.router.navigate(['/']);
}

So from here, I believe that I need to parse the query params from the returnUrl and return them separately.

Well that was unfortunate. I worked through how to parse out the search parameters and pass the query parameters separately, only to find that the router also had a method to simply navigateByUrl.

const path = url.substring(0, i);
const search = url.substring(i + 1);
const params = new URLSearchParams(search);
const queryParams: Record<string, any> = {};
params.forEach((value, key) => {
  queryParams[key] = value;
});
this.router.navigate([path], { queryParams });
this.router
.navigateByUrl(url);

I prefer to use the built-in method.

With all of that out of the way, I’m now able to login to the website with my username/password, and 2FA.

Done!

Done?

Hardly…

Although the front-end UI is now guarded to prevent access, anyone can still call the endpoints and access logs or even change the user credentials. We need to guard against unauthenticated users. The question is how? We have a few options.

  • Sessions – PHP stores a file in the temp folder with a unique session id. A cookie is sent to the client to provide to subsequent requests. Sessions expire and require re-authentication. SECURITY RISK!! If the sessions are stored in /tmp/, or anther folder accessible by other clients using the same server, then someone could hijack a users session.
  • Secrets Manager – A token can be generated and stored in the secrets database once authenticated. Tokens can persist as long as I wish.
  • Error Log Database – Similar to secrets database, but a separate table can handle multiple users and sessions. Tokens can persist as long as I wish.

I’ve personally had problems initiating sessions in the past with BlueHost. Mainly their temp folder for storing session data wasn’t configured for write access. It was fixed after I had contacted support, but shortly afterwards the problem came back. I’m a bit weary of using PHP sessions since the Hosting provider has control over it working or not. I suppose we can test Hostinger. If it works out of the box, then maybe we are fine. At least we have a couple backup solutions.

Thinking a bit through this, I should setup the login endpoint to start the session and store the username. It should also store a variable indicating if the user is authenticated – which means it should check if 2FA is enabled. If that is the case, I can return the necessity for OTP with the response, saving the user an extra trip to see if OTP is enabled.

Okay – just about everything that needs to be guarded, is. In addition, the login endpoint lets me know if I need to prompt for 2FA. All good right? Nope. As I monitor each API request, I see PHP is sending me a different cookie. Something isn’t configured correctly. Since I’m making requests from localhost over to another domain, I think I may not have setup CORS properly.

PHP Options Handler for CORS
function allow()
{
    global $HTTP_ORIGIN;
    // Set CORS headers
    header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
    header("Access-Control-Allow-Headers: Content-Type, Authorization");
    header('Access-Control-Allow-Credentials: true');
    header("Access-Control-Allow-Origin: $HTTP_ORIGIN");
    http_response_code(HTTP_STATUS_OK);
}
.htaccess CORS
# Rewrite OPTIONS requests (cors) to a specific handler script or endpoint
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ options-handler.php [L]

<IfModule mod_headers.c>
  <If "%{REQUEST_METHOD} =~/^(GET|POST)$/">
    # Browser already got past CORS OPTIONS
    Header set Access-Control-Allow-Origin "*"
    Header set Access-Control-Allow-Methods "GET, POST"
    Header set Access-Control-Allow-Headers "Content-Type, Authorization"
    Header set Access-Control-Allow-Credentials "true"
  </If>
</IfModule>

What I’m seeing is that I’m allowing credentials. I’ve change the headers to also allow “Cookie”. I went in and changed all of my API requests to pass http option of “withCredentials: true”. The preflight request for OPTIONS passes with flying colors. I’m now seeing some pretty mean looking CORS errors with provisional headers instead of the actual headers. Caching is already disabled on my end.

I think the problem may be with PHP setting session cookies for https only.

I’ve set withCredentials back to false. The CORS errors go away, but the cookie keeps changing with each request.

I’m running in circles trying to use session_set_cookie_params and all kinds of stuff trying to control if they are for secure sites only, accessible to http only, same site, etc., etc. As I keep digging deeper to troubleshoot this problem, I feel like I’m making things more riskier. Cookies are not a great way to maintain session state. I think I’m just going to revert back to using the secrets manager as a temporary solution.

I just did a major rewrite. Rather than continue to modify a ton of files to tweak the http requests, I consolidated the http request logic into a class called “Api” with two methods – get and post. From here, I actually change the get method into a post by converting the parameters into a query string and posting an empty object for the parameters. Why? Because no I’m posting the authentication token with every request. And guess what? It works. My services have become much simpler to manage.

API service
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Observable, catchError, throwError } from 'rxjs';
import { AuthService } from './AuthService';
import { environment } from './environments/environment';

@Injectable({
  providedIn: 'root'
})
export class Api {

  constructor(
    private http: HttpClient,
    private auth: AuthService
  ) { }

  get<T>(stub: string, data: Record<string, any>): Observable<T> {
    const params = new URLSearchParams(data);
    const url = `${stub}?${params}`;
    return this.post(url, {});
  }
  post<T>(stub: string, data: Record<string, any>): Observable<T> {
    data['token'] = this.auth.token();
    const httpOptions = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json'
      }),
    };
    return this.http.post<T>(`${environment.api}/${stub}`, data, httpOptions).pipe(
      catchError((response: HttpErrorResponse) => {
        try {
          const error = response.error.error;
          return throwError(() => new Error(error));
        } catch (e) {
          return throwError(() => new Error("Unexpected API response"));
        }
      })
    );
  }
}

So now my endpoints are locked down. Awesome. Down side – only one person can log in at one time. Once they provide valid credentials, anyone who is authenticated will lose their authentication. Not good… but okay for now as I’m the only one managing it.

Can we change to bearer token authentication? I’d like to keep GET requests as such, and avoid polluting the parameters. Will we get the CORS error again? Let’s try it out. It should be easier to implement now that the http request logic is centralized.

Yes – the authorization header can be sent. No CORS error here. I did find something odd. With HttpHeaders I couldn’t append/set headers after it was created. The reason I found this is that I don’t want to add the Authorization header unless I have a token.

let headers = new HttpHeaders({
  'Content-Type': 'application/json'
});
headers.append('Authorization', `Bearer ABC`);
console.log(headers.keys()); // Content-Type

headers.set('Authorization', `Bearer 123`);
console.log(headers.keys()); // Content-Type

headers = new HttpHeaders({
  'Content-Type': 'application/json',
  'Authorization': 'Bearer ABC123'
});
console.log(headers.keys()); // Content-Type, Authorization

Well… well, this is silly. Not intuitive at all. Apparently the append and set methods return a clone of the updated object, rather than mutating the object.

let headers = new HttpHeaders({
  'Content-Type': 'application/json'
});
let copy = headers.append('Authorization', `Bearer ABC`);
console.log(copy.keys()); // Content-Type, Authorization

I think what I’ll do is just modify an object before I send in the headers.

const headers: Record<string, string | number> = {
  'Content-Type': 'application/json'
};
const token = this.auth.token();
if (token !== undefined) {
  headers['Authorization'] = `Bearer ${token}`;
}
const httpOptions = {
  headers: new HttpHeaders(headers)
};

Got it! I ran into some trouble reading the Authentication header on the server. My host isn’t able to let me use $_SERVER[‘Authorization’] to get the header, so I reverted to apache_request_headers()[‘Authorization’].

<?php
function get_token()
{
    if (!function_exists('apache_request_headers')) {
        // $_SERVER['Authorization'] may work...
        Show::error('Unauthorized', HTTP_STATUS_INTERNAL_SERVER_ERROR);
        exit;
    }
    $headers = apache_request_headers();
    if (!isset($headers['Authorization'])) {
        Show::error('Unauthorized', HTTP_STATUS_UNAUTHORIZED);
        exit;
    }
    $authorizationHeader = trim($headers['Authorization']);
    $parts = explode(' ', $authorizationHeader);
    if (!isset($parts[0]) || $parts[0] !== 'Bearer' || !isset($parts[1])) {
        Show::error('Unauthorized', HTTP_STATUS_BAD_REQUEST);
        exit;
    }
    return $parts[1];
}

What’s left… page refresh. Every time the page reloads during development, I need to login and enter the 2FA. It would be nice to persist the authorization token across page reloads.

Cookies are messy as you need to parse out values from a large list of cookies. Instead, let’s just persist our value in local storage.

Persisting authentication in local storage
import { Injectable } from '@angular/core';
import { AuthenticationStatus } from './AuthenticationStatus';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  status?: AuthenticationStatus;
  constructor() {
    const json = localStorage.getItem('authentication');
    if (json === null) return;
    try {
      this.status = JSON.parse(json);
    } catch (e) {
    }
  }
  setStatus(status: AuthenticationStatus) {
    this.status = status;
    localStorage.setItem('authentication', JSON.stringify(status));
  }
  isLoggedIn() {
    return this.status?.authenticated ?? false;
  }
  canBypass2FA(): boolean {
    if (this.status === undefined) return false;
    return this.status.authenticated && !this.status.otp_required;
  }
  token() {
    return this.status?.token;
  }
}

That worked out well. I can now refresh the page and see where I left off without logging in. Currently, the only way to logout is to remove the key in local storage or mess with it so that it isn’t valid JSON. We need a button to log out.

The simplest thing to do is to just tell the authentication service to log out the user, and then redirect them to the login page.

Logout
logout() {
  this.auth.logout();
  const { pathname, search, hash } = window.location;
  const extras = {
    queryParams: {
      returnUrl: `${pathname}${search}${hash}`
    }
  };
  this.router.navigate(['login'], extras);
}

AuthService.ts

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

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  status?: AuthenticationStatus;
  constructor() {
    const json = localStorage.getItem('authentication');
    if (json === null) return;
    try {
      this.status = JSON.parse(json);
    } catch (e) {
    }
  }
  setStatus(status: AuthenticationStatus) {
    this.status = status;
    localStorage.setItem('authentication', JSON.stringify(status));
  }
  logout() {
    localStorage.removeItem('authentication');
    this.status = undefined;
  }
  isLoggedIn() {
    return this.status?.authenticated ?? false;
  }
  canBypass2FA(): boolean {
    if (this.status === undefined) return false;
    return this.status.authenticated && !this.status.otp_required;
  }
  token() {
    return this.status?.token;
  }
}

Wrap Up

I had about a half-day to work on the project as I had other things to tend to. I finally got authentication wired up. It took a while to get here. I still see a few holes to fill before I’d like to drop this project and move on, but for the most part, we’ve got it down. I’ve also found myself using the project as a tool quite often, so it’s definitely got it’s usefulness.

  • Learned to generate scaffolding components on the command line
  • Wired up routes – correctly
  • Guard routes with Authentication and 2FA
  • Created a Login form
  • Created a One-Time Password form
  • Created endpoints:
    • login
    • 2FA enabled
    • 2FA verify
  • Return user to original url requested after authenticating
  • Centralized http get/post logic for all api calls
  • Token authentication
    • Attempted to use PHP sessions but ran into trouble with CORS in localhost over http requesting remote domain over https.
    • Changed to post tokens as a separate field with every request
    • Changed to use Authorization header to send bearer tokens
  • Locked down most endpoints to require token authentication before returning any data.
  • Persist authentication token in local storage
  • Add ability to log out

I had mentioned a few holes. What was that about? The issue is that if I log into the production website, my credentials in the localhost development site no longer work. Everything displays as empty data. I can manually log out and log back in. I need automatically to log the user out and redirect them to the login page. In addition, my error handling isn’t that great on the log list, and log item pages. I need to wire them up to use the snack bar to communicate errors. They should also display a progress spinner when fetching data – similar to how I did the same thing for editing the login credentials yesterday.

Once that is done, there are some smaller holes to patch in regards to rate limiting. Currently, someone can make a brute force attack and try to login. I need to stop that after three attempts and lock the account for a few minutes. Even if they get past the login page, they still need to get past the 2FA. 2FA is also susceptible to a brute force attack – but with less combinations. It only takes 1,000,000 requests within 90 seconds to get past the OTP. I need to wire up a cooldown period for 2FA as well.

Discover more from Lewis Moten

Subscribe now to keep reading and get access to the full archive.

Continue reading