Distributed Node #10

Authentication & authorization

Difference between Authentication and Authorization

Authentication Authorization
Determines whether users are who they claim to be Determines what users can and cannot access
Challenges the user to validate credentials (for example, through passwords, answers to security questions, or facial recognition) Verifies whether access is allowed through policies and rules
Usually done before authorization Usually done after successful authentication
Generally, transmits info through an ID Token Generally, transmits info through an Access Token
Generally governed by the OpenID Connect (OIDC) protocol Generally governed by the OAuth 2.0 framework
Example: Employees in a company are required to authenticate through the network before accessing their company email Example: After an employee successfully authenticates, the system determines what information the employees are allowed to access

Authentication Strategy in a Microservice Architecture

Authentication & Authorization on each service

The security logic needs to be implemented repeatedly in each microservice. This causes code duplication between the services.

It distracts the development team from focusing on their domain main service.

Hard to maintain and monitor

Different auth flows & security issues

Netflix

Global Authentication & Authorization Service

The authorization check is a business concern. What specific user roles are allowed to do on the service is governed by business rules. Therefore, the authorization concern should not be handled in the global authentication service.

This strategy increases the latency of processing requests

More maintainance

API Gateway Authentication 

 The authorization concern should not be handled in the global authentication service.

It reduces the latency (call to Authentication service) and ensures the authentication process is consistent across the application.

Stateful vs Stateless authentication 

Stateful authentication

the server creates a session for the user after authenticating. The session id is then stored as a cookie in the user's browser and the user session store in the cache or database. When the client tries to access the server with a given session id, the server attempts to load the user session context for the session store, checks if the session is valid, and decides if the client has to access the desired resource or rejects the request.

Stateless authentication 

stores the user session on the client-side. A cryptographic algorithm signs the user session to ensure the session data’s integrity and authority.

Each time the client requests a resource from the server, the server is responsible for verifying the token claims sent as a cookie.

Brief Introduction to JSON Web Token (JWT)

A JWT is an open standard (RFC-7519) that defines a mechanism for securely transmitting information between two parties. The JWT token is a signed JSON object that contains a list of claims which allow the receiver to validate the sender's identity.

The purpose of using the JWT token is for a stateless authentication mechanism. Stateless authentication stores the user session on the client-side.

HMAC

hash-based message authentication code

 Is a specific type of message authentication code (MAC) involving a cryptographic hash function and a secret cryptographic key.

HMAC_SHA256("key", "The quick brown fox jumps over the lazy dog") = f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8

Why not use just SHA256?

An HMAC is a MAC which is based on a hash function. The basic idea is to concatenate the key and the message, and hash them together. Since it is impossible, given a cryptographic hash, to find out what it is the hash of, knowing the hash (or even a collection of such hashes) does not make it possible to find the key. The basic idea doesn't quite work out, in part because of length extension attacks, so the actual HMAC construction is a little more complicated. For more information, browse the hmac tag on Cryptography Stack Exchange, especially Why is H(k||x) not a secure MAC construction?, Is H(k||length||x) a secure MAC construction? and HMAC vs MAC functions. There are other ways to define a MAC, for example MAC algorithms based on block ciphers such as CMAC.

Our strategy

User service
Nest.js/TS/Etc

Chat service
nodes

Public :443 HTTPS

OAuth 2 provider

OAuth interceptor

HTTP/REST

WS/Socket.IO

Users DB

Users cache
Messages cache

User disabled notification

Private Kubernetes cluster

HTTPS

WS

Broker

Get user details on connection

Forward auth / HMAC signature headers validation

Facebook OAuth flow

?

OAuth 2.0 is the industry-standard protocol for authorization. OAuth 2.0 focuses on client developer simplicity while providing specific authorization flows for web applications, desktop applications, mobile phones, and living room devices. This specification and its extensions are being developed within the IETF OAuth Working Group.

But how to integerate Trafik & Facebook ?

Traefik ForwardAuth

The ForwardAuth middleware delegates authentication to an external service. If the service answers with a 2XX code, access is granted, and the original request is performed. Otherwise, the response from the authentication server is returned.

# Forward authentication to example.com
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: test-auth
spec:
  forwardAuth:
    address: https://example.com/auth

Configuration Example

K8s config

And the service itself

const http = require('http');
const fetch = require('node-fetch');
const crypto = require('crypto');

const cookieDomain =  process.env.COOKIE_DOMAIN || 'lvh.me';
const app_url = process.env.APP_URL || 'https://lvh.me:8125';
const middleware_url = process.env.MIDDLEWARE_URL || 'https://auth.lvh.me:8125';
const client_id = process.env.CLIENT_ID;
const client_secret = process.env.CLIENT_SECRET;
const cookieName =  process.env.COOKIE_NAME || 'check';

const port = 8125;
const redirect_uri = `${middleware_url}/auth`;
const authRedurectUrl = `https://www.facebook.com/v4.0/dialog/oauth?client_id=${client_id}&redirect_uri=${redirect_uri}&scope=email,user_friends&response_type=code&auth_type=rerequest&display=popup`;

function shaSignature(text) {
    return crypto.createHmac('sha256', client_secret).update(text).digest('base64');
}

function parseGetParams(str = '') {
    return str.split('&').reduce((res, pair) => {
        const parts = pair.split('=');
        res[parts[0]] = parts[1] || true;
        return res;
    }, {});
}

function validCookies(cookie = '') {
    const cookies = cookie.split(/\s*;\s*/);
    const check = cookies.find((c) => c.startsWith(cookieName));
    if (!check) {
        console.log('@ No check cookies', check, 'cookieName=', cookieName, 'cookies', cookies);
        return false;
    }

    const parts = check.slice(cookieName.length + 1).split('^');
    if (parts.length !== 2) {
        console.log('@ No parts', parts);
        return false;
    }

    if (shaSignature(parts[1]) !== parts[0]) {
        console.log('@ SHA is different', parts, shaSignature(parts[1]));
        return false;
    }

    const userData = parts[1].split('|');
    if (userData.length !== 4) {
        console.log('@ wron user data', userData);
        return false;
    }

    if (Number(userData[3]) < new Date().getTime()) {
        console.log('@ bad date ', Number(userData[3]), new Date().getTime());
    }
    return Number(userData[3]) > new Date().getTime();
}

function fetchUserData(code) {
    return fetch(
        `https://graph.facebook.com/v4.0/oauth/access_token?client_id=${client_id}&client_secret=${client_secret}&redirect_uri=${redirect_uri}&code=${code}`
    )
    .then((resp) => {
        if (resp.status >= 400) {
            throw new Error('Failed to obtain access_token ' + resp.status);
        }
        return resp.json();
    })
    .then(({ access_token, expires_in }) => {
        return fetch(
            `https://graph.facebook.com/me?access_token=${access_token}&fields=id,email,first_name,last_name`
        )
        .then((resp) => resp.json())
        .then((resp) => [resp, access_token, expires_in]);
    });
}

http.createServer(function (request, response) {
    console.log('--> req', request.url, request.headers.cookie)
    if (request.url.startsWith('/auth')) {
        const params = parseGetParams(request.url.split('/auth?')[1]);
        fetchUserData(params.code)
            .then(([data, acess_token, expiration]) => {
                const expires = new Date().getTime() + Number(expiration);
                const body = `${data.email}|${encodeURIComponent(data.first_name)}|${encodeURIComponent(data.last_name)}|${expires}`;
                const hash = shaSignature(body);
                const resp = hash + '^' + body;
                response
                    .writeHead(301, {
                        Location: app_url,
                        'Set-Cookie': `${cookieName}=${resp}; domain=${cookieDomain}; HttpOnly; Max-Age=${Number(expiration) / 1000};`,
                    })
                    .end();
            })
            .catch((e) => response
                    .writeHead(500, {
                        'Content-Length': Buffer.byteLength(e.message),
                        'Content-Type': 'text/plain',
                    })
                    .end(e.message)
            );
    } else if (validCookies(request.headers.cookie)) {
        response.writeHead(200).end();
    } else {
        response.writeHead(302, { Location: authRedurectUrl }).end();
    }
}).listen(port, () => console.log(`Server started on port: ${port}`));

100 lines implementation :)

No Express

No Typescript

No Babel

No Nest

kind: Deployment
apiVersion: apps/v1
metadata:
  namespace: default
  name: auth-middleware
  labels:
    app: auth-middleware

spec:
  replicas: 1
  selector:
    matchLabels:
      app: auth-middleware
  template:
    metadata:
      labels:
        app: auth-middleware
    spec:
      containers:
        - name: auth-middleware
          image: localhost:32000/node-auth-middleware
          env:
            - name: COOKIE_DOMAIN
              value: "nwsd.tk"
            - name: APP_URL
              value: "https://nwsd.tk"
            - name: MIDDLEWARE_URL
              value: "https://auth.nwsd.tk"
            - name: CLIENT_ID
              value: "XXX" 
            - name: CLIENT_SECRET
              value: "XXX"        
          ports:
            - name: web
              containerPort: 8125

---
apiVersion: v1
kind: Service
metadata:
  name: auth-middleware

spec:
  ports:
    - protocol: TCP
      name: web
      port: 8125
  selector:
    app: auth-middleware

Service Deployment

Now the Facebook part

HTTPS is mandatory

Even for the localhost

apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: sso
spec:
  forwardAuth:
    address: http://auth-middleware:8125
    authResponseHeaders: 
        - "X-Forwarded-User"
    trustForwardHeader: true
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: ingressroutetls
  namespace: default
spec:
  entryPoints:
    - websecure
  routes:
  - match: Host(`nwsd.tk`)
    kind: Rule
    services:
    - name: whoami
      port: 80
    middlewares:
    - name: default-sso@kubernetescrd
  tls:
    certResolver: myresolver
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: traefik-sso
spec:
  entryPoints:
    - websecure
  routes:
  - match: Host(`auth.nwsd.tk`)
    kind: Rule
    services:
    - name: auth-middleware
      port: 8125
  tls:
    certResolver: myresolver

Traefik configuration

vvyshko@kb:~/Projects/distributed-node/client-app$ dig A www.nwsd.tk

; <<>> DiG 9.16.1-Ubuntu <<>> A www.nwsd.tk
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 21769
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;www.nwsd.tk.                   IN      A

;; ANSWER SECTION:
www.nwsd.tk.            282     IN      A       127.0.0.1

;; Query time: 4 msec
;; SERVER: 127.0.0.53#53(127.0.0.53)
;; WHEN: нд бер 14 18:34:19 EET 2021
;; MSG SIZE  rcvd: 56


sudo microk8s kubectl port-forward service/traefik 443:443

And the domain

You can use any DNS service you like.

I am going to use free dot.tk for the demo

sudo microk8s kubectl port-forward service/traefik 443:443

Forward Traefik https to localhost

DNS SPOOFING (DNS CACHE POISONING)

DNS spoofing is a type of attack in which a malicious actor intercepts DNS request and returns the address that leads to its own server instead of the real address. Hackers can use DNS spoofing to launch a man-in-the-middle attack and direct the victim to a bogus site that looks like the real one, or they can simply relay the traffic to the real website and silently steal the information.

DNS hijacking attack types

There are four basic types of DNS redirection:

  • Local DNS hijack — attackers install Trojan malware on a user’s computer, and change the local DNS settings to redirect the user to malicious sites.
  • Router DNS hijack — many routers have default passwords or firmware vulnerabilities. Attackers can take over a router and overwrite DNS settings, affecting all users connected to that router.
  • Man in the middle DNS attacks — attackers intercept communication between a user and a DNS server, and provide different destination IP addresses pointing to malicious sites.
  • Rogue DNS Server — attackers can hack a DNS server, and change DNS records to redirect DNS requests to malicious sites.

Thank You!

Distributed Node #10

By Vladimir Vyshko

Distributed Node #10

Authentication & authorization

  • 482