-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into dependabot/npm_and_yarn/public-site/docs/vit…
…e-4.4.12
- Loading branch information
Showing
58 changed files
with
2,095 additions
and
1,379 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
# Radix example: front proxy | ||
|
||
**This is application files that should be reviewed as sample input to the use of oauth proxy in your application. Have a look and include what you need in your app.** | ||
|
||
**We will not guarantee that this configuration is up to date** | ||
|
||
This is a sample application that showcases how to use an authentication proxy to provide authentication for a SPA front-end that calls an protected API. The API is only accessible for all authenticated Equinor users. It is possible to further restrict this to only allow for a specific [role to have access to the API](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps) | ||
|
||
This pattern can be used to wrap existing or new components in an application with a single authentication mechanism. It is an alternative to implementing authentication directly in clients, e.g. using [MSAL](https://github.com/AzureAD/microsoft-authentication-library-for-js). | ||
|
||
 | ||
|
||
The `frontend` components is only accessible through the `auth-proxy`. The `auth-proxy` ensures that the client is correctly authenticated. | ||
|
||
The `api` is accessible on its own url. It protects itself by validating that the access token is signed by the AzureAD private key, that it's not [expired](https://tools.ietf.org/html/rfc7519#section-4.1.4) and that [audience](https://tools.ietf.org/html/rfc7519#section-4.1.3) matches its application/resource ID. Its easy to extend to also authorize based on user role, see [index.js](api/index.js) | ||
|
||
## Requirements | ||
|
||
Note: In Equinor AccessIT you need to have the role `Application Developer` for access `AZURE ACTIVE DIRECTORY` to be able to work with Azure AD. See [slack message](https://equinor.slack.com/archives/C04E6T3AQ/p1567530111001700) for more information. | ||
|
||
### API | ||
|
||
To make use of this authentication pattern, you will need to: | ||
|
||
- Create an **app registration** in Azure AD for the API | ||
- Get the API app's **client ID** (from Azure AD, also called _application ID_) | ||
- Define a scope named `user_impersonation` for the API. | ||
|
||
The `apis` **client ID** is used to tell Azure which resource a user is attempting to access when communicating via the auth_proxy. **Scopes** define the specific actions applications can be allowed to do on a user's behalf, in this case, what the auth_proxy needs to do to accomplish its job. We'll bind the **role** to an AD group, where any user that has access to this group will get a access token where the role `Radix` is set. | ||
|
||
To generate a **scope**, in the APIs Azure AD app, go to "Expose an API" and generate a **scope** called `user_impersonation`. Both `Admin and users` should be allowed to consent. Verify that the scopes name is in form `api://${client ID}/user_impersonation` | ||
|
||
#### API - Role binding access policy (RBAC) | ||
|
||
Role-based access control (RBAC) is a popular mechanism to enforce authorization in applications. When using RBAC, an administrator grants permissions to roles, and not to individual users or groups. The administrator can then assign roles to different users and groups to control who has access to what content and functionality. E.g. all requests to the api under `api/admin` might require an `ADMIN` role, while requests under `api/geology` require `GEOLOGIST` role. | ||
|
||
In its simples form only a single role exist, which is needed to to any request to the API. The role is granted to a single group, basically only allowing requests from users in that group. | ||
|
||
To generate a **role**, in the APIs Azure AD app, go to "Manifest" and update the "appRoles" value of the json doc. You need to replace the id with your own [UUID](https://www.uuidgenerator.net/): | ||
|
||
``` | ||
"appRoles": [ | ||
{ | ||
"allowedMemberTypes": [ | ||
"User" | ||
], | ||
"description": "An admin user.", | ||
"displayName": "Admin", | ||
"id": "d1c2ade8-98f8-45fd-aa4a-6d06b947c661", | ||
"isEnabled": true, | ||
"lang": null, | ||
"origin": "Application", | ||
"value": "Admin" | ||
} | ||
] | ||
``` | ||
|
||
To grant a AD user or group a **role**, in the APIs Azure AD app, go to "Overview" and click the link for "Manage application in local directory". This will open Enterprise application overview of the app we're working on. Go to "Users and Groups" -> "Add User" -> select an AD group your part of (e.g. `Radix Playground Users`) and grant it the **role** "Admin". | ||
|
||
Important: Users who are not part of the AD group you granted the Radix role to, will still be able to authenticate, get a valid access token, and get access to the Client. It's up to the API to authorize based on the **role**. This enable the possibility to limit API calls based on which **role** a user has. | ||
|
||
### Client | ||
|
||
To make use of this authentication pattern, you will need to: | ||
|
||
- Create a second **app registration** in Azure AD for the client | ||
- Get the app's **client ID** (from Azure, also called _application ID_) | ||
- Get a **client secret** (generated in Azure) | ||
- Create a **cookie secret** (generated locally) | ||
- Extend the app **API permissions** with the **scope** defined for API | ||
|
||
The **client ID** is used to tell Azure which application a user is attempting to access. The **client secret** proves to Azure that the authentication request is coming from a legitimate source (the `auth-proxy`). And the **cookie secret** is used to encrypt/decrypt the authentication cookie set in the user's browser, so that it is only readable by the `auth-proxy`. | ||
|
||
The **client ID** is not a secret, and is set directly as an environment variable (`OAUTH2_PROXY_CLIENT_ID`). The **client secret** and **cookie secret** should be handled securely and never committed to git. | ||
|
||
To generate the **client secret**, in the Azure app, go to "Certificates & secrets", then generate a new "Client secret". | ||
|
||
To generate the **cookie secret**, you can use this command: | ||
|
||
python -c 'import os,base64; print base64.urlsafe_b64encode(os.urandom(16)) | ||
|
||
## Running locally | ||
|
||
To run the example locally, ensure that the values for `OAUTH2_PROXY_CLIENT_ID`, `OAUTH2_PROXY_CLIENT_SECRET`, `OAUTH2_PROXY_COOKIE_SECRET` and `API_RESOURCE_ID` are set in a `.env` file (this will be excluded from git; you can use the `.env.template` file as a… template 🤓). | ||
|
||
You can now run `docker-compose up`. | ||
|
||
The main endpoint (which is routed through `auth-proxy`) will be available at http://localhost:8000. The `frontend` and `api` endpoints will be at http://localhost:8001 and http://localhost:8002, respectively, if you need direct access. `api` will will return 403 if you do not provide a valid auth token in the [request header](https://swagger.io/docs/specification/authentication/bearer-authentication/). | ||
|
||
## Running in Radix | ||
|
||
You will need to change the value for the `OAUTH2_PROXY_CLIENT_ID`, `OAUTH2_PROXY_SCOPE` and `API_RESOURCE_ID` environment variables in `radixconfig.yaml`. You can then [set up the application](https://www.radix.equinor.com/guides/configure-an-app/#registering-the-application) in Radix. | ||
|
||
The two [secrets](https://www.radix.equinor.com/docs/topic-concepts/#secret) that must be configured in the Radix Web Console are `OAUTH2_PROXY_CLIENT_SECRET` and `OAUTH2_PROXY_COOKIE_SECRET`. Note that the **cookie secret** does not need to match the one used locally. | ||
|
||
The application should then build and deploy, and it will be availble at `https://<app-name>.app.radix.equinor.com/`. The `auth-proxy` component will be exposed via this endpoint. | ||
|
||
## Further development | ||
|
||
The implementations of `frontend` and `api` should of course be specific to your needs. | ||
|
||
If `frontend` is a single-page app you'll want to include its build process in `frontend/Dockerfile`. You can also consider changing routing rules in the `frontend/nginx.conf` file — for instance, the application assumes that static files are served from the `/app` directory. | ||
|
||
The `api` component represents a backend. It will receive the following headers with every request: | ||
|
||
- `Authorization`: The [access token](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens) (JWT) provided by Azure for the authenticated user. The backend should perform the appropriate [validation](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens#validating-tokens) of this token. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
FROM node:18-alpine3.17 | ||
|
||
# Create app directory | ||
WORKDIR /app | ||
|
||
# Install app | ||
COPY package*.json ./ | ||
RUN npm ci --only=production | ||
|
||
# Bundle app source | ||
COPY . . | ||
|
||
# Start server | ||
EXPOSE 8002 | ||
USER 1001 | ||
CMD [ "node", "--max-http-header-size=64000", "index.js" ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
"use strict"; | ||
|
||
const fetch = require("node-fetch"); | ||
const express = require("express"); | ||
const PORT = process.env.PORT || 8002; | ||
const HOST = process.env.HOST || "0.0.0.0"; | ||
const app = express(); | ||
const jwt = require("jsonwebtoken"); | ||
const azureADPublicKey = []; | ||
const resourceID = process.env.API_RESOURCE_ID; | ||
|
||
// get public keys used for signing tokens from azure ad | ||
const getADPublicKeys = async url => { | ||
try { | ||
const response = await fetch(url); | ||
const json = await response.json(); | ||
json.keys.forEach(key => { | ||
azureADPublicKey[ | ||
key.kid | ||
] = `-----BEGIN CERTIFICATE-----\n${key.x5c}\n-----END CERTIFICATE-----`; | ||
}); | ||
} catch (error) { | ||
console.log(error); | ||
} | ||
}; | ||
|
||
/** | ||
* authorize request using Authorization header, expecting a Bearer token | ||
* req - request Request<Dictionary<string>> | ||
* [roles] - array of roles. If empty skip check. The token is authorized if it has any of the roles | ||
* returns - isAuthorized = true/false. | ||
*/ | ||
const isAuthorized = (req, roles) => { | ||
let token = req.header("authorization").replace("Bearer ", ""); | ||
let isAuthorized = false; | ||
|
||
try { | ||
const decodedToken = jwt.decode(token, { complete: true }); | ||
const publicKey = azureADPublicKey[decodedToken.header.kid]; | ||
|
||
const validatedToken = jwt.verify(token, publicKey, { | ||
audience: resourceID | ||
}); | ||
if (roles && roles.length > 0) { | ||
isAuthorized = | ||
validatedToken.roles && | ||
roles.some(role => | ||
validatedToken.roles.some(userRole => userRole === role) | ||
); | ||
} else { | ||
isAuthorized = true; | ||
} | ||
} catch (err) { | ||
console.log(err); | ||
} | ||
return isAuthorized; | ||
}; | ||
|
||
// Generic request handler | ||
app.get("*", (req, res) => { | ||
console.log(`Request received by the API: ${req.method} ${req.originalUrl}`); | ||
// if (!isAuthorized(req, ["Radix"])){ | ||
if (!isAuthorized(req, [])) { | ||
res.sendStatus(403); | ||
return; | ||
} | ||
|
||
let output = ` | ||
Request received by the API: ${req.method} ${req.originalUrl} | ||
Headers: ${JSON.stringify(req.headers, null, 2)} | ||
`; | ||
|
||
res.send(output); | ||
}); | ||
|
||
// get public keys used for signing tokens from azure ad | ||
getADPublicKeys(process.env.AZURE_AD_PUBLIC_KEY_URL); | ||
|
||
// Start server | ||
app.listen(PORT, HOST); | ||
console.log(`Running on http://${HOST}:${PORT}`); |
Oops, something went wrong.