Protecting an API and UI

Protecting an API and UI

Certain applications expose both an application programming interface (API) and user interface (UI). The Maverics Orchestrator is capable of protecting both types of resources. This can be achieved by defining policies for the distinct resources (URL paths), and by using Service Extensions where necessary.

Protecting an API

To start, we will protect just API resources found under /api/. To protect the API, the Orchestrator will parse and validate JWT bearer tokens using the IsAuthorized Service Extension. We will also leverage the HandleUnauthorized Service Extension to return custom response codes and content.

Note: Please refer to the “JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens” RFC for a comprehensive guide on how to validate JWTs.

Our maverics.yaml file:

appgateways:
  - name: exampleApp
    location: /
    upstream: https://app-internal.example.com
    handleUnauthorizedSE:
      funcName: HandleUnauthorized
      file: authorize.go

    policies:
      # Authorize API requests.
      - location: /api/
        authentication:
          allowUnauthenticated: true
        authorization:
          isAuthorizedSE:
            funcName: IsAuthorized
            file: authorize.go

Our authorize.go file:

package main

import (
  "context"
  "crypto/x509"
  "encoding/pem"
  "fmt"
  "net/http"
  "strings"

  "maverics/app"
  "maverics/jwt"
  "maverics/log"
)

const (
  publicKey = `-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA92CTz/cxJVVVKAov1RrVSI35wgpEbS3QANgYX6TFnVvnORePnyJm
RQ08HAGUnB7KWje37ti3UvvG403hSKEPinPqJS+7LgUII+Ud9QnH1L4AlYjXfp5n
MvgqzMVForveEYGK6bB9QVdwd413gKHN57OhEXKGTkMIeYwxI6Ih77yNZAouMuzB
bV9/mp8cngb7Tcjb+sb01Ix5ix1aSAl/ffkJvhaGqwbeMUb3iXZazPSRPnKfGNUN
4XbTsREiUrk/1jcLZe9buQ9Lz0RSPKsU2cASyd5EYxsvwniT4tCETqbGwI5+/CD4
R7ocg/MsDR8MeyWsQ4tp38Dc48MkeJ5Z/QIDAQAB
-----END RSA PUBLIC KEY-----`
)

type ctxKey int

var unauthorizedErrKey ctxKey

type unauthorizedErr struct {
  error string
  code  int
}

func IsAuthorized(
  ag *app.AppGateway,
  rw http.ResponseWriter,
  req *http.Request,
) bool {
  log.Debug("msg", "determining if request is authorized")

  // Ensure the request's context is updated before returning. The request's context
  // is used for error handling.
  var ctx context.Context
  defer func() { *req = *req.Clone(ctx) }()

  log.Debug("msg", "retrieving access token from request")
  splitToken := strings.Split(req.Header.Get("Authorization"), "Bearer ")
  if len(splitToken) < 2 {
    ctx = context.WithValue(req.Context(), unauthorizedErrKey, &unauthorizedErr{
      error: `{"error": "missing bearer token"}`,
      code:  http.StatusUnauthorized,
    })
    return false
  }
  accessToken := splitToken[1]

  log.Debug("msg", "parsing raw JWT")
  token, err := jwt.ParseSigned(accessToken)
  if err != nil {
    ctx = context.WithValue(req.Context(), unauthorizedErrKey, &unauthorizedErr{
      error: fmt.Sprintf(`{"error": "failed to parse access token: %s"}`, err),
      code:  http.StatusUnauthorized,
    })
    return false
  }

  log.Debug("msg", "verifying signature of JWT")
  block, _ := pem.Decode([]byte(publicKey))
  parsedKey, err := x509.ParsePKCS1PublicKey(block.Bytes)
  if err != nil {
    log.Error("msg", "failed to parse public key: "+err.Error())
    ctx = context.WithValue(req.Context(), unauthorizedErrKey, &unauthorizedErr{
      error: http.StatusText(http.StatusInternalServerError),
      code:  http.StatusInternalServerError,
    })
    return false
  }

  var claims = make(map[string]interface{})
  err = token.Claims(parsedKey, &claims)
  if err != nil {
    ctx = context.WithValue(req.Context(), unauthorizedErrKey, &unauthorizedErr{
      error: fmt.Sprintf(`{"error": "failed to validate JWT: %s"}`, err),
      code:  http.StatusUnauthorized,
    })
    return false
  }

  log.Debug("msg", "determining if request is authorized")
  scope, _ := claims["scope"].(string)
  if !strings.Contains(scope, "read write") {
    ctx = context.WithValue(req.Context(), unauthorizedErrKey, &unauthorizedErr{
      error: `{"error": "missing required scope"}`,
      code:  http.StatusUnauthorized,
    })
    return false
  }

  return true
}

func HandleUnauthorized(
  ag *app.AppGateway,
  rw http.ResponseWriter,
  req *http.Request,
) {
  log.Debug("msg", "handling custom unauthorized response")

  v := req.Context().Value(unauthorizedErrKey)
  err, ok := v.(*unauthorizedErr)
  if !ok {
    log.Error("msg", "unexpected value found on request's context")
    rw.WriteHeader(http.StatusInternalServerError)
    return
  }

  http.Error(rw, err.error, err.code)
}

Adding Protection for the UI

Next, we want to ensure the UI is properly protected. This can be achieved by adding a few simple policies.

Our updated maverics.yaml file:

appgateways:
  - name: exampleApp
    location: /
    upstream: https://app-internal.example.com
    handleUnauthorizedSE:
      funcName: HandleUnauthorized
      file: authorize.go

    policies:
      # By default, protect all resources.
      - location: /
        authentication:
          idps:
            - azure
        authorization:
          allowAll: true
   
      # Allow open access to static assets.
      - location: ~ \.(jpg|png|ico|svg|ttf|js|css|gif)$
        authentication:
          allowUnauthenticated: true
        authorization:
          allowAll: true
   
      # Authorize API requests.
      - location: /api/
        authentication:
          allowUnauthenticated: true
        authorization:
          isAuthorizedSE:
            funcName: IsAuthorized
            file: authorize.go