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