dp-authorisation

module
v2.31.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 26, 2024 License: MIT

README

dp-authorisation V2

Authorisation is broken down into two parts:

  • JWT token parsing: read the Authorization header of a request, and parse the JWT token contained in it. From the JWT token the user ID and list of groups the user belongs to is extracted and returned in the EntityData type. This functionality is within the jwt package. See the package readme for more details
  • Permissions check - the action that the user is taking will have a permission associated with it. The permissions check does a lookup to see if the requested permission is granted to the user, or the groups that the user belongs to. This functionality is within the permissions package. See the package readme for more details

Usage

The permission check will typically be wrapped around an entire endpoint via middleware, but it can also be checked within a handler with more complex logic if needed.

Authorisation config

The config values for authorisation are the same regardless of how authorisation is applied to a service. The authorisation package provides a configuration type that can be embedded within an existing service config type.

  type Config struct {
    ...
    AuthorisationConfig *authorisation.Config
  }

A set of default configuration values can be retrieved using the authorisation.NewDefaultConfig() function. These can be used for local development and testing. The config values should be set as environment variables when running in an environment.

JSON Web Token (JWT) RSA Public Signing Key Map

In order to verify a JWT's validity, the RSA public signing keys used to sign the JWT generated by the AWS Cognito User Pool are required. There are 2 RSA public signing keys associated with a User Pool. The Key ID (KID) header in the JWT is used to determine which of these keys has been used to sign the JWT. The map is of the pointer form *map[string]string.

When consuming this library from a service you have 2 options:

  • supply the map manually, or
  • allow the library to obtain the keys and set the map automatically for you
    • for this, when creating a new instance, simply set the third argument to nil
Option 1 - Add authorisation middleware to API endpoints

For the typical case of adding authorisation as middleware, the JWT parsing and permissions checking has been bundled into a single Middleware type.

Create a new instance of authorisation middleware
  • Option 1 - set RSA Public Signing key map automatically:

    authorisationMiddleware, err := authorisation.NewFeatureFlaggedMiddleware(ctx, authorisationConfig, nil)
    
  • Option 2 - set RSA Public Signing key map manually:

    authorisationMiddleware, err := authorisation.NewFeatureFlaggedMiddleware(ctx, authorisationConfig, &jwtRSAPublicSigningKeyMap)
    

    where jwtRSAPublicSigningKeyMap has the form (may come from a config for example):

      {
          "GHB723n83jw=": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0TpTemKodQNChMNj1f/NF19nM",
          "HUJB8hw29js=": "MIICIjANBgkqBUHJHUJOIOIJIOH&*B(IHUGYCgKCAgEA0TpTemKodQNChMNj1f/NF19nM"
      }
    

Using the NewFeatureFlaggedMiddleware constructor will use the Enabled config value to automatically apply a feature flag to authorisation. If the flag is disabled, a no-op instance of middleware will be used. This minimises the amount of code required to apply a feature flag to authorisation. Endpoints can still be wrapped with the authorisation middleware, but it will just act as a pass-through if authorisation is disabled. Should you want to create a middleware instance without a feature flag, use the NewMiddlewareFromConfig constructor function instead.

Wrap endpoints using the authorisationMiddleware.Require function
    r.HandleFunc("/v1/users", authorisationMiddleware.Require("users:create", api.CreateUserHandler)).Methods(http.MethodPost)

The above example shows the POST /v1/users endpoint being wrapped with authorisation middleware, requiring the caller to have the users:create permission.

Add a health check for the underlying permissions checker
    if err := hc.AddCheck("permissions cache health check", authorisationMiddleware.HealthCheck); err != nil {
        hasErrors = true
        log.Error(ctx, "error adding check for permissions cache", err)
    }
Add a health check for the underlying jwt keys request against identity service
    if err := hc.AddCheck("identity client jwt keys health check", authorisationMiddleware.IdentityHealthCheck); err != nil {
        hasErrors = true
        log.Error(ctx, "identity client jwt keys health check", err)
    }
Call close on the middleware instance when the service is shut down
   if err := svc.authorisationMiddleware.Close(ctx); err != nil {
        log.Error(ctx, "failed to close authorisation middleware", err)
        hasShutdownError = true
    }
Creating a mock middleware instance for unit testing

A mock for the Middleware interface is available for unit testing:

    import (
        authorisation "github.com/ONSdigital/dp-authorisation/v2/authorisation/mock"
    )
    
    ...

    middlewareMock := &authorisation.MiddlewareMock{
        RequireFunc: func(permission string, handlerFunc http.HandlerFunc) http.HandlerFunc {
            return handlerFunc
        },
    }
Creating a mock ZebedeeClient instance for unit testing

A mock for the ZebedeeClient interface is available for unit testing:

    import (
        authorisation "github.com/ONSdigital/dp-authorisation/v2/authorisation/mock"
    )
    
    ...

    zebedeeIdentity = &mock.ZebedeeClientMock{
        CheckTokenIdentityFunc: func(ctx context.Context, token string) (*dprequest.IdentityResponse, error) {
            return &dprequest.IdentityResponse{
                Identifier: "[email protected]",
            }, nil
        },
    }
Option 2 - Add authorisation within a handler (not via middleware)

If the authorisation for a service requires something more complex than middleware around a handler, the implementation will depend on the service's particular requirements. Though it will still come down to the two fundamental pieces of the authorisation - JWT token parsing, and permissions checking. Refer to the readme's for the JWT parser and permissions checker for more information on creating and using them.

It should also be considered how a feature flag may be applied in this case. The authorisation config type contains an Enabled boolean for this purpose, but usage of the flag will need to be implemented.

The JWT token parsing could potentially be done within middleware, and the EntityData that comes from the JWT could be stored in the request content for later use within the handler. Other than that the JWT parser could be used directly within the handler.

Once the JWT token is parsed into EntityData, it can be passed to the permissions checker to determine if the user has access. It's likely at this point that additional data will be needed by the permissions checker to make a decision. This is where the attributes parameter of the permissions checker is used - for example to set a collection ID:

  permission := "legacy.read"
  attributes := map[string]string{"collection_id": "collection123"}

  hasPermission, err := permissionChecker.HasPermission(ctx, entityData, permission, attributes)

Mock types for the JWTParser and PermissionsChecker interfaces are available under the github.com/ONSdigital/dp-authorisation/v2/authorisation/mock import path.

Component testing with authorisation

The authorisationtest package provides test JWT tokens, and a fake permissions API that can be used in component tests.

Using the fake permissions API

Instantiate the fake permissions API in the test component, then read the URL value to set the permissions API URL in the config:

    fakePermissionsAPI := authorisationtest.NewFakePermissionsAPI()
    c.Config.AuthorisationConfig.PermissionsAPIURL = fakePermissionsAPI.URL()

Once the config value is set for the permissions API, use the authorisation code (middleware or permissions checker) as it is used in the service.

JWT tokens for component tests

The JWT tokens provided emulate users who are member of different groups. They have been generated to work with the public key that's provided in the default configuration.

To use the test JWT tokens within a component test, register a step that adds the token as a header (example taken from the Identity API):

import (
    "github.com/ONSdigital/dp-authorisation/v2/authorisationtest"
)

...
ctx.Step(`^I am an admin user$`, c.adminJWTToken)
...

func (c *IdentityComponent) adminJWTToken() error {
  err := c.apiFeature.ISetTheHeaderTo(api.AccessTokenHeaderName, authorisationtest.AdminJWTToken)
  return err
}

Then the JWT token can be added to a request in the feature file:

  Given I am an admin user
  When ...
Creating a new instance of authorisation middleware and retrieving the Parsed token Entity Data

The following example demonstrates how to retrieve the Parsed token EntityData from the authorisation middleware instance dynamically, i.e without specifying the RSA public signing keys.

// main.go
package main

import (
        "context"
        "fmt"

        "github.com/ONSdigital/dp-authorisation/v2/authorisation"
)

func main() {
        // the following retrieves auth config values used in local dev and testing. For other environments, ensure you set IDENTITY_WEB_KEY_SET_URL env variable – https://github.com/ONSdigital/dp-authorisation/blob/master/v2/authorisation/config.go#L5-L15
        cfg := authorisation.NewDefaultConfig()
        cfg.JWTVerificationPublicKeys = nil
        ctx := context.Background()
        authorisationMiddleware, err := authorisation.NewFeatureFlaggedMiddleware(ctx, cfg, nil)
        if err != nil {
                fmt.Println(err)
        }
        // NOTE: If you retrieve the token from florence, ensure to strip out the `Bearer` string preceding the token.
        token := "eyJraWQiOiIyYTh2WG1JSzY3WlozaEZaXC9Ed1FBVGd2cVpnUkJGanV1VmF2bHczekV3bz0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJiOTIyM2VjZi0wYzMxLTRmZWUtODVkOC0zZmJlNjAwM2M1MWQiLCJjb2duaXRvOmdyb3VwcyI6WyJhZmU5ODA0OS0wNzU4LTRiYTgtYmQwNy1mOTY4ZjllYmFkMWQiXSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLmV1LXdlc3QtMS5hbWF6b25hd3MuY29tXC9ldS13ZXN0LTFfUm5tYTlscDJxIiwiY2xpZW50X2lkIjoiZGZjbTRvbms2MHJtc3NhOHJuN3NoamtpdiIsIm9yaWdpbl9qdGkiOiJlMGYzMjdiYS02MzA0LTQ0MzEtYjZmMy1jNTUwZTgwZTllYmIiLCJldmVudF9pZCI6IjYyZmM3Y2NjLWM0MTgtNGFmNC1hMDhlLThlZmU2NDU5MWUwNSIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoiYXdzLmNvZ25pdG8uc2lnbmluLnVzZXIuYWRtaW4iLCJhdXRoX3RpbWUiOjE2NTY1MDE3NTUsImV4cCI6MTY1NjUwNTM1NSwiaWF0IjoxNjU2NTAxNzU1LCJqdGkiOiI0MzQ5OTc2Zi1kMjIyLTQwNGQtYTJkMy0zNTM4NjRjZGVjOWYiLCJ1c2VybmFtZSI6IjFjZjFlMDI1LTJhZDYtNGQ1NC04NmRiLTEzYTlhMjcxMTg1OCJ9.F6w7yuEh-tThF8Q_qH7oOwq5wNSvhDLltCKVTEHvyOa15CsMBepoAOu3XW6xHO-S6z60I17t3u4KGCI6iOsPclo7nGQsoq0bpxsgMAoPjZhOCk7qzDjbHBvk_MA2NLR8tDbxwfdlDiCQviKK3rLj6xT_n9jdcGhDrf58AO2gNNHxrGIg83iWhG650OS0AdGtc1rcVudlNoIpbwKOk1cLtfj44jozc4ZWI34MgGuz5bFtCJ39ZPAJuA8bebNa0krb4CW7W8Il0MnUO-h6wMfocZr6HpfrKoMJHGRvBuh6uVnULRGL1ZjgfqjduCSYF7r24PLHS1V-nIbaa-4-WDIojA"
        entityData, err := authorisationMiddleware.Parse(token)
        if err != nil {
                fmt.Println(err)
        }
        fmt.Printf("%+v\n", entityData)
}
$ go run main.go
{"created_at":"2022-06-29T11:25:38.057373Z","namespace":"main","event":"GetPermissionsBundle: starting permissions bundle request","severity":3,"data":{"uri":"http://localhost:25400/v1/permissions-bundle"}}
{"created_at":"2022-06-29T11:25:38.063618Z","namespace":"main","event":"GetPermissionsBundle: request successfully executed","severity":3,"data":{"resp.StatusCode":200}}
{"created_at":"2022-06-29T11:25:38.063869Z","namespace":"main","event":"GetPermissionsBundle: returning requested permissions to caller","severity":3}
&{UserID:1cf1e025-2ad6-4d54-86db-13a9a2711858 Groups:[afe98049-0758-4ba8-bd07-f968f9ebad1d]}
An example of parsing the JWT token and permission checking seperately

The authorisation library both parses the JWT token and checks authorisation permissions as a combined convenience function. It is also possible to perform these steps separately as shown below:

// main.go
package main

import (
        "context"
        "fmt"
        "reflect"

        "github.com/ONSdigital/dp-authorisation/v2/authorisation"
        "github.com/ONSdigital/dp-authorisation/v2/authorisationtest"
        "github.com/ONSdigital/dp-authorisation/v2/permissions"
)

func main() {
        ctx := context.Background()
        // the following retrieves auth config values used in local dev and testing. For other environments, ensure you set IDENTITY_WEB_KEY_SET_URL env variable – https://github.com/ONSdigital/dp-authorisation/blob/master/v2/authorisation/config.go#L5-L15
        cfg := authorisation.NewDefaultConfig()
        ExpectedEntityData := permsdk.EntityData{
                UserID: "[email protected]",
                Groups: []string{"role-admin"},
        }
// Parse JWT token and retrieve entity Data
        jwtParser, err := authorisation.NewCognitoRSAParser(cfg.JWTVerificationPublicKeys)
        if err != nil {
                println(err)
        }
        // NOTE: If you retrieve the token from florence, ensure to strip out the `Bearer` string preceding the token.
        token := "eyJraWQiOiJOZUtiNjUxOTRKbz0iLCJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYWFhYWFhYS1iYmJiLWNjY2MtZGRkZC1lZWVlZWVlZWVlZWUiLCJkZXZpY2Vfa2V5IjoiYWFhYWFhYWEtYmJiYi1jY2NjLWRkZGQtZWVlZWVlZWVlZWVlIiwiY29nbml0bzpncm91cHMiOlsicm9sZS1hZG1pbiJdLCJ0b2tlbl91c2UiOiJhY2Nlc3MiLCJzY29wZSI6ImF3cy5jb2duaXRvLnNpZ25pbi51c2VyLmFkbWluIiwiYXV0aF90aW1lIjoxNTYyMTkwNTI0LCJpc3MiOiJodHRwczovL2NvZ25pdG8taWRwLnVzLXdlc3QtMi5hbWF6b25hd3MuY29tL3VzLXdlc3QtMl9leGFtcGxlIiwiZXhwIjo5OTk5OTk5OTk5OTksImlhdCI6MTU2MjE5MDUyNCwianRpIjoiYWFhYWFhYWEtYmJiYi1jY2NjLWRkZGQtZWVlZWVlZWVlZWVlIiwiY2xpZW50X2lkIjoiNTdjYmlzaGs0ajI0cGFiYzEyMzQ1Njc4OTAiLCJ1c2VybmFtZSI6ImphbmVkb2VAZXhhbXBsZS5jb20ifQ.ZmZkZlrAtFxG5PnfC7dOru_KykJJ5f5bu7YkpCaNMwjXtBM8hWmiWk88QGfbx9kqI1wYs479cFrZ0FablR_38ek6RH9yAVaxTk7ZKOBUqSbVbIB-82B5iRXI8vLquZYjZEunH7LDv0kfZbsqoCZCe3nAJU5aV-hVMF1Cbz2LgIymRqMFqDxD2YIu5RgRHc71FtPebNfMTFCmnTs2v5b4KOqDNZZuab7eLMc-B941M6XyfdF7I6RRfvxw7xTv-qi6ZhGzkbe7K2rlxUmSwjQRDPYrOD7qji_V7yxon9okPyvpTHp-8yaHyrVv1CUCHX67c3OSRT7x3gZqRcPYpEZmScyj7M38Kwn04CKcNqc4ouozIBqhtkBgnCWJuaj1wl7AxQDRR5_F_IS962Y8t2IfU-UurqoZAZvQqWWyeBVJB3aIKrhSJHx62ayZVjd3u2za2WS8aZT97pjEuKLjSoYcgdEqnL9_fKdZc4Vv3QBZmtj_rZsb-zOrj2u_kMox8g-uaIC6ehkNucmM-HEfSuTA7nf_pPNw9c6HLDXJizGWMBVf18K94HPFTyWtJWB7yhXCuV9Kulp9iVGEn8230e6mn7ui0z8lU8R-KpZm3_aPTXBXKsUVdsoj0ZK5sd4y5ARdZ5BOGurT5NpMsw8avW-CqMF0dPY2kmUv3EtBE6dkvdg"

        ParsedEntityData, err := jwtParser.Parse(token)
        if err != nil {
                println(err)
        }
        fmt.Printf("%+v\n", ParsedEntityData)
        fmt.Println("valid parsed JWT: ", reflect.DeepEqual(ParsedEntityData, &ExpectedEntityData))

        // Check authorisation permissions
        fakePermissionsAPI := authorisationtest.NewFakePermissionsAPI()

        permissionChecker := permissions.NewChecker(
                ctx,
                fakePermissionsAPI.URL(),
                cfg.PermissionsCacheUpdateInterval,
                cfg.PermissionsMaxCacheTime,
        )

        permission := "users:create"

        hasPermission, err := permissionChecker.HasPermission(ctx, *ParsedEntityData, permission, nil)
        if err != nil {
                println(err)
        }
        fmt.Println("entity has permission: ", hasPermission)
}
$ go run main.go
&{UserID:[email protected] Groups:[role-admin]}
valid parsed JWT:  true
{"created_at":"2022-05-12T08:27:57.344261Z","namespace":"main","event":"GetPermissionsBundle: starting permissions bundle request","severity":3,"data":{"uri":"http://127.0.0.1:57766/v1/permissions-bundle"}}
{"created_at":"2022-05-12T08:27:57.345428Z","namespace":"main","event":"GetPermissionsBundle: request successfully executed","severity":3,"data":{"resp.StatusCode":200}}
{"created_at":"2022-05-12T08:27:57.345538Z","namespace":"main","event":"GetPermissionsBundle: returning requested permissions to caller","severity":3}
entity has permission:  true

Directories

Path Synopsis
Package permissions provides library functions to determine if a user/service has a particular permission.
Package permissions provides library functions to determine if a user/service has a particular permission.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL