Tuesday, September 1, 2020

NGINX authentication using OKTA

 

In this post we will review the steps to integrate NGINX with OKTA. Most of the work displayed here is based on the post Use nginx to Add Authentication to Any Application.

Side note: In case you want an alternative for OKTA, check my other post: Using NGINX as a Reverse Proxy for Google OAuth 2.0.


We have an NGINX that serves as a reverse proxy for a backend site. Now we want to add authentication of the users using OKTA. This would cause any new user arriving to NGINX to authenticate with OKTA before proceeding to the backend site. To implement this we use the vouch-proxy which is handling the OIDC authentication protocol.


The Requests Flow

The requests flow is listed below:








  1. The user accesses the site URL
  2. NGINX configured with auth request sends the request headers to the vouch proxy
  3. The vouch proxy does not find any access token in the headers, and returns HTTP status 401
  4. NGINX redirects the user to the vouch proxy using a login URL
  5. The vouch proxy redirects the user to the OKTA authentication service.
  6. The user logins to OKTA
  7. OKTA redirects the user back to the site URL, and adds an access token header
  8. NGINX configured with auth request sends the request headers to the vouch proxy
  9. The vouch proxy does finds the access token in the headers, and sends it to the OKTA authentication service, which returns the user details.
  10. The vouch proxy returns HTTP status 200
  11. The NGINX proxy the request to the backend site



    Implementation on Kubernetes


    To implement on kubernetes, we use the sample backend site: http://okta-headers.herokuapp.com

    We add two DNS entries
    • app1.alontest.com 
    • login.alontest.com
    As this example uses NodePort, both of these IPs point to one of the kubernetes nodes IP.
    Note that both of these DNS entries must share a common domain prefix (alontest.com) to allow cookies sharing.


    Create an OKTA Application


    To create an OKTA application, register to OKTA development site.

    Once you have completed the registration, you will get into the OKTA admin, for example, in my case:

    The OKTA base URL is shown in the OKTA dashboard:





    Click on Applications, and then Add Application of type Web.
    The application URLs should be updated with our vouch proxy URL.
    In addition, the client ID and the client secret should be copied from here to the vouch proxy configuration (see in the next sections).







    The NGINX Entities

    The NGINX is deployed in kubernetes, and includes a deployment, a service and a configMap.

    The NGINX service:


    apiVersion: v1
    kind: Service
    metadata:
    name: nginx-service
    spec:
    selector:
    app: okta
    type: NodePort
    ports:
    - port: 80
    targetPort: 8080
    name: tcp-api
    protocol: TCP
    nodePort: 30000




    The NGINX deployment:



    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: nginx-deployment
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: okta
    template:
    metadata:
    labels:
    app: okta
    spec:
    terminationGracePeriodSeconds: 1
    containers:
    - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
    env:
    volumeMounts:
    - name: nginx-config
    mountPath: /etc/nginx/nginx.conf
    subPath: nginx.conf
    volumes:
    - name: nginx-config
    configMap:
    name: nginx-config


    And the NGINX configMap:



    apiVersion: v1
    kind: ConfigMap
    metadata:
    name: nginx-config
    data:
    nginx.conf: |-
    user nginx;
    worker_processes 10;

    error_log /dev/stdout warn;
    pid /var/run/nginx.pid;

    events {
    worker_connections 10;
    }

    http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    resolver 8.8.8.8;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log off;
    sendfile on;
    port_in_redirect off;
    proxy_max_temp_file_size 0;

    server {
    listen 8080;
    server_name localhost;

    # Any request to this server will first be sent to this URL
    auth_request /vouch-validate;

    location = /vouch-validate {
    # This address is where Vouch will be listening on
    proxy_pass http://vouch-service:80/validate;
    proxy_pass_request_body off; # no need to send the POST body

    proxy_set_header Content-Length "";
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # these return values are passed to the @error401 call
    auth_request_set $auth_resp_jwt $upstream_http_x_vouch_jwt;
    auth_request_set $auth_resp_err $upstream_http_x_vouch_err;
    auth_request_set $auth_resp_failcount $upstream_http_x_vouch_failcount;
    }

    error_page 401 = @error401;

    # If the user is not logged in, redirect them to Vouch's login URL
    location @error401 {
    return 302 http://login.alontest.com:30001/login?url=http://$http_host$request_uri&vouch-failcount=$auth_resp_failcount&X-Vouch-Token=$auth_resp_jwt&error=$auth_resp_err;
    }

    location / {
    proxy_pass http://okta-headers.herokuapp.com;
    }
    }
    }




    Some notes for the NGINX configuration:
    • Our backend site is http://okta-headers.herokuapp.com
    • We use auth_request to validate any request with the vouch proxy
    • The location /vouch-validate is the configuration of how to access the vouch proxy
    • In case of HTTP status 401, we redirect the user (using HTTP status 302) to the vouch-proxy. Notice that we send a "url" parameter in the query string to point back to where should the user be redirected once the authentication is complete.


    The Vouch Proxy Entities

    The vouch proxy is deployed in kubernetes, and includes a deployment, a service and a configMap.

    The vouch proxy service:


    apiVersion: v1
    kind: Service
    metadata:
    name: vouch-service
    spec:
    selector:
    app: vouch
    type: NodePort
    ports:
    - port: 80
    targetPort: 9090
    name: tcp-api
    protocol: TCP
    nodePort: 30001



    The vouch proxy deployment:


    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: vouch-deployment
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: vouch
    template:
    metadata:
    labels:
    app: vouch
    spec:
    terminationGracePeriodSeconds: 1
    containers:
    - name: vouch
    image: voucher/vouch-proxy
    imagePullPolicy: IfNotPresent
    env:
    volumeMounts:
    - name: vouch-config
    mountPath: /config
    volumes:
    - name: vouch-config
    configMap:
    name: vouch-config


    And the vouch proxy configMap:


    apiVersion: v1
    kind: ConfigMap
    metadata:
    name: vouch-config
    data:
    config.yml: |-
    vouch:
    logLevel: debug
    testing: false
    listen: 0.0.0.0 # VOUCH_LISTEN
    port: 9090 # VOUCH_PORT
    allowAllUsers: true
    jwt:
    maxAge: 240
    compress: true

    cookie:
    name: VouchCookie
    domain: alontest.com
    secure: false
    maxAge: 14400
    sameSite: lax

    session:
    name: VouchSession

    headers:
    jwt: X-Vouch-Token # VOUCH_HEADERS_JWT
    querystring: access_token # VOUCH_HEADERS_QUERYSTRING
    redirect: X-Vouch-Requested-URI # VOUCH_HEADERS_REDIRECT
    claims:
    - groups
    - given_name

    test_url: http://yourdomain.com
    post_logout_redirect_uris:
    - http://myapp.yourdomain.com/login
    - https://oauth2.googleapis.com/revoke
    - https://myorg.okta.com/oauth2/123serverid/v1/logout?post_logout_redirect_uri=http://myapp.yourdomain.com/login



    oauth:
    provider: oidc
    client_id: 0oauxxx18zEdyIR4hxxx
    client_secret: aqvxxxTHi4uzqyFCVvGWTIXFiTFxxx27IloTN0_H
    auth_url: https://dev-137493.okta.com/oauth2/default/v1/authorize
    token_url: https://dev-137493.okta.com/oauth2/default/v1/token
    user_info_url: https://dev-137493.okta.com/oauth2/default/v1/userinfo
    end_session_endpoint: https://dev-137493.okta.com/oauth2/default/v1/logout
    scopes:
    - openid
    - email
    - profile
    callback_url: http://login.alontest.com:30001/auth


    Some notes for the vouch proxy configuration:
    • Use allowUsers:true, means that once a user is authenticated in OKTA, vouch proxy would permit its access, without additional limitations
    • The cookie.domain specifies the common DNS prefix for the vouch proxy DNS and the NGINX DNS
    • The cookie.secure: false enables us to use HTTP connections (and not HTTPS)
    • The oauth.client_id and oauth.client_secret should be copied from the general section of the application that is created in OKTA admin site
    • The oauth URLs parameters use the OKTA site URL (notice not to use the admin site by mistake, like I did at the beginning...)
    • The callback URL is used to access back to the vouch proxy after the OKTA authentication is done


    Final Note

    In this post we have reviewed an NGINX and OKTA integration.

    This is a simple example, but for production, you would probably change few of the configuration, like using a registering real DNS entries, using SSL for the NGINX.

     

    6 comments:

    1. auth_request_set $email $upstream_http_x_auth_request_email;
      set_encode_base64 $encoded_string $email;
      proxy_set_header Authorization "Basic $encoded_string";

      $email in nginx log is getting correct value, but for encoded_string $email is coming as empty.

      ReplyDelete
      Replies
      1. This part of the wonders of NGINX.
        The encoding is taking place before the auth request is sent.
        If you want encoded email, you should try to return it encoded directly from the auth server.

        Delete
    2. Encode works on nginx if I do set_encode_base64 $encoded_string "test@example.com" this works, but the $email is not getting value in my example.

      In your example I saw $auth_resp_jwt used as "return 302 http://login.alontest.com:30001/login?url=http://$http_host$request_uri&vouch-failcount=$auth_resp_failcount&X-Vouch-Token=$auth_resp_jwt&error=$auth_resp_err;"

      $auth_resp_jwt is the variable set from auth_request_set.
      --
      I am not sure how to get encoded email from Oauth proxy.

      ReplyDelete
      Replies
      1. to get the encoded email, you will need to return it from the auth request.
        In our case, the vouch server is the auth request service, so you need it to return the encoded header. You can fork the vouch server from https://github.com/vouch/vouch-proxy, and add the encoded header.

        Delete
    3. Hi Alon and Thanks for the post!

      Correct me if I'm wrong, but, after authentication against Okta, when the user will make another request to the backend site, NGINX will still send the request to Vouch in order to make sure this request is authenticated, right?

      NGINX is not handling any aspect of the authentication, it will just "consult" with some authentication end-point for every request and the authentication end-point will decide whether the request is already authenticated or not by checking headers/cookies/parameters?

      Thanks!

      ReplyDelete
      Replies
      1. That's correct, NGINX will address the vouch for every request.

        Delete