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