Protection d’une API et d’une interface utilisateur

Protection d’une API et d’une interface utilisateur

Certaines applications présentent à la fois une interface de programmation d’application (API, Application Programming Interface) et une interface utilisateur (UI, User Interface). L’orchestrateur Maverics est capable de protéger les deux types de ressources. Pour ce faire, il convient de définir des politiques pour les différentes ressources (chemins d’accès URL) et d’utiliser des extensions de services si nécessaire.

Protéger une API

Dans un premier temps, nous ne protégerons que les ressources de l’API qui se trouvent sous /api/. Pour protéger l’API, l’orchestrateur analysera et validera les jetons JWT bearer tokens à l’aide de l’extension de services IsAuthorized. Nous utiliserons également l’extension de services HandleUnauthorized pour renvoyer des codes de réponse et des contenus personnalisés.

Remarque : reportez-vous au RFC « JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens » pour obtenir un guide complet sur la manière de valider les jetons JWT.

Notre fichier maverics.yaml :

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

Notre fichier authorize.go :

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)
}

Ajout d’une protection pour l’interface utilisateur

À présent, nous allons nous assurer que l’interface utilisateur est correctement protégée. Pour ce faire, il convient d’ajouter quelques politiques simples.

Notre fichier maverics.yaml mis à jour :

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