# sv-auth-client

Client for communicating with sv-auth. This npm package contains classes and helpers for communicating with the sv-auth GraphQL system.

Use cases include:
* Registering permissions on the auth system for your product.
* Working with tokens from the auth system.
* Wrapping the UI of your product behind the auth system.
* Converting a token into a user and checking their permissions.

[Changelog](changelog.md) - See the latest changes to sv-auth-client.

## OS Support

The expectation is that this application will be installed in Linux using [sv-kubernetes](https://github.com/simpleviewinc/sv-kubernetes/).

## Prerequisites

* [sv-graphql-client](https://github.com/simpleviewinc/sv-graphql-client/)

## Installation

Using npm:
```bash
npm install @simpleview/sv-auth-client
```

Using yarn:
```bash
yarn install @simpleview/sv-auth-client
```

## Update

To update to the latest version, just rerun the install command.

## Setup

For integrating your project with auth, please see the [Setup Instructions](setup.md).

## Implementation & Usage

* [AuthClient](#AuthClient)
* [canIds](#canIds)
* [DirectiveGetUser](#DirectiveGetUser)
* [DirectiveCheckPerm](#DirectiveCheckPerm)
* [User](#User)

### AuthClient

The `AuthClient` class is for communicating with the authentication system which provides some caching and ease of use for working with `User` objects.

* args
	* graphUrl - The URL of the GraphQL server that you wish to communicate with. Most implementations should likely point to the live GraphQL endpoint at https://graphql.simpleviewinc.com/.
	* cacheDuration - The duration of how long cache entries should remain in the AuthClient cache. Defaults to 1 hour, generally you should not pass this setting unless unit testing.

#### AuthClient.getUser

This method wraps the call to `auth.users_current` in a caching layer to ensure that it's performant and is properly updating if a user's permissions have changed.

Generally you will want to make this call very early in your GraphQL stack in order to make the user available on context for all calls to access.

* args
	* token - string - The jwt token retrieved from the auth system.
	* acct_id - string - The acct_id the user is accessing.
	* headers - object - HTTP headers, x-sv-permissionjson can be used for overwriting user permissions in unit tests.

Returns `auth_user`.

```js
const user = authClient.getUser({
	token,
	acct_id : "0"
});
```

### getTokenFromHeaders

Extracts the token from the `authorization` header.

```js
const { getTokenFromHeaders } = require("@simpleview/sv-auth-client");
const server = new ApolloServer({
	...
	context: ({ req }) => {
		return {
			token : getTokenFromHeaders(req.headers)
		};
	}
});
```

### AdminPrefix

`AdminPrefix` can be loaded into the `sv-graphql-client` `GraphServer` to use as a client library for accessing `admin` in GraphQL.

```js
const { AdminPrefix } = require("@simpleview/sv-auth-client");
const { GraphServer } = require("@simpleview/sv-graphql-client");
const graphServer = new GraphServer({ graphUrl : GRAPH_URL, prefixes : [AdminPrefix] });
```

### AuthPrefix

`AdminPrefix` can be loaded into the `sv-graphql-client` `GraphServer` to use as a client library for accessing `auth` in GraphQL.

```js
const { AuthPrefix } = require("@simpleview/sv-auth-client");
const { GraphServer } = require("@simpleview/sv-graphql-client");
const graphServer = new GraphServer({ graphUrl : GRAPH_URL, prefixes : [AuthPrefix] });
```

### canIds(perm, node_type, bindings)

This function is identical to `User.canIds` but requires you to pass in the `bindings` manually, it should only be used in cases where you cannot use `DirectiveCheckPerm`. See the documentation for [`User.canIds`](#User.canIds) for how it functions.

### DirectiveGetUser

`DirectiveGetUser` can be used to convert a token into a user session.

When using the directive, if your system passes the request headers object on context, you can utilize the header `x-sv-permissionjson` to set the permissions for a specific request. This can only be used if the token is `sv : true` and it reduces the permissions of that specific request. This makes unit tests easier as you don't need to create roles and users for each specific cross section of permission testing.

#### Adding DirectiveGetUser

The recommended approach is to split out a separate file in your schema to include the directive.

Note: You will need to provide `name`, which should be `PREFIX_getUser` and `graphUrl` which should point to the version of auth you are accessing.

```js
const {
	getDirectiveGetUser
} = require("@simpleview/sv-auth-client");

module.exports = getDirectiveGetUser({ name : NAME, graphUrl : AUTH_URL })

// example
module.exports = getDirectiveGetUser({ name : "redirects_checkPerm", graphUrl : "https://graphql.simpleviewinc.com/link/auth-v2/" })
```

If you are not using `schemaLoader`, `getDirectiveGetUser()` returns `{ schemaDirectives, typeDefs }` and you can manually integrate those with your existing directives and schema files.

#### Using DirectiveGetUser

The directive is usually best applied to your root resolver, and requires that your root resolver has an acct_id as a top-level filter parameter. This is best used in conjunction with the `checkPerm` resolver to enforce the permissions on that user.

In the below example, the top-level prefix converts the token into a user, then the specific resolver enforces permissions based on that user.

```
query {
	prefix(acct_id: String): prefix_query @prefix_getUser

	type prefix_query {
		some_endpoint: some_result @prefix_checkPerm(sv: true)
	}
}
```

### DirectiveCheckPerm

`DirectiveCheckPerm` can be used to enforce permissions to access a resolver. It can enforce permissions based on `sv`, `permissions` or retrieve `object_bindings`.

#### Adding the Directive
The recommended approach is to split out a separate file in your schema to include the directive.

Note: You will need to provide `name`, which should be `PREFIX_checkPerm` and `graphUrl` which should point to the version of auth you are accessing.

```js
const {
	getDirectiveCheckPerm
} = require("@simpleview/sv-auth-client");

module.exports = getDirectiveCheckPerm({ name : NAME, graphUrl : AUTH_URL });

// example
module.exports = getDirectiveCheckPerm({ name : "redirects_checkPerm", graphUrl : "https://graphql.simpleviewinc.com/link/auth-v2/" });
```

If you are not using `schemaLoader`, `getDirectiveGetUser()` returns `{ schemaDirectives, typeDefs }` and you can manually integrate those with your existing directives and schema files.

#### Using the directive

* perms - [String] - An array of permissions this endpoint requires.
* sv - Boolean - Whether this endpoint requires an SV user.
* bindings
  *  perms - [String] - The permissions for bindings to return.
  *  node_types - [String] - The node_types to return binding information on.

```
query {
	# require SV user
	some_query: some_result @prefix_checkPerm(sv: true)
	# require permission
	some_query: some_result @prefix_checkPerm(perms: ["foo"])
	# require multiple permissions
	some_query: some_result @prefix_checkPerm(perms: ["foo", "bar"])
	# pull in binding information
	some_query: some_result @prefix_checkPerm(bindings: { node_types : ["dms.accounts"] })
	some_query: some_result @prefix_checkPerm(bindings: { perms : ["dms.accounts.read", "dms.accounts.write"] })
	some_query: some_result @prefix_checkPerm(bindings: { node_types : ["dms.accounts", "dms.groups"], perms : ["dms.accounts.read", "dms.accounts.write", "dms.accounts.remove"] })
}
```

### User

`User` is returned by `AuthClient.getUser` but it can also be used without `AuthClient` when you want to convert the return from `auth.current` into a user which you wish to check permissions on.

```js
const { User } = require("@simpleview/sv-auth-client");
const result = await graphServer.auth.current({
	acct_id : "0",
	fields : `
		success
		message
		doc {
			permissionJson
		}
	`
});
const user = new User(result.doc);
user.can(["some.perm.name"]);
```

#### User.can

* perms - array of strings - Returns boolean as to whether the user has all requested permissions.

```js
const allow = user.can(["cms.something.another", "cms.another.permission"]);
```

#### User.canIds

This function is used for assisting in filtering down queries based on what IDs a user has access to for a given permission and node_type. In order to use this function you must specify `bindings` in the `DirectiveCheckPerm` for the GraphQL endpoint being executed. If you are not using that mechanic, then use the generic `canIds` function rather than the `User.canIds` variant.

Returns `true` if the user has root access to this permission/node_type.

Returns `false` if the user lacks all access to this permission/node_type. Note, that lacking access to this permission does not mean the user cannot see any of the content type, as they may have permissions to a node_type higher in the hierarchy.

Returns `string[]` of ids if the user has object bindings.

* perm - string - Name of the permission.
* node_type - string - Name of the node_type to return the ids for.

```js
const account_ids = user.canIds("dms.accounts.read", "dms.account");
const group_ids = user.canIds("dms.accounts.read", "dms.group");

const result = await sqlQuery(`
	SELECT * FROM accounts a
	WHERE
		(
			@account_ids is null OR account_id IN (@account_ids)
			OR
			@group_ids is null OR group_id IN (@group_ids)
		)
`, {
	account_ids : account_ids === true ? null : account_ids,
	group_ids : group_ids === true ? null : group_ids
})
```

#### User.toPlain

Convert the `User` object back to a plain JS object.




### OAuth 2.0

This is a library of functions to enable a product application to authenticate a user using the OAuth 2.0 framework. This works by handshaking codes and challenge tokens between the product and auth and uses the auth login to generate the access tokens for the product. There is a separate class for Express but Express is not required to use OAuth 2.0 authentication.

* User visits product application page
* If there is a session with a token and refresh token then serves content
* Creates `state` random string and stores in session
* Creates `code_verifier` random string and stores in session
* Redirects to auth `/oauth2/login/` endpoint passing:
	* `response_type` - must be "code"
	* `code_challenge_method` - must be "S256"
	* `client_id` - The `client_id` of the application. e.g. `admin` or `cms`. This id should be registered with auth as an `oauth_client`.
	* `redirect_uri` - The url which auth will redirect to upon successful login or if already logged in. This should match the `redirect_uri` registered with auth as an `oauth_client`. e.g. `https://test-account.admin.simpleviewinc.com/oauth2/callback/`.
	* `state` - Generated above
	* `code_challenge` - `Base64URLEncode(SHA256(code_verifier))`
	* `sv_auth_params` - JSON string containing an object. The properties of this object are added to the auth login url as extra parameters. This can be used to add e.g. the `account_name` or the `product`.
* If not logged in then redirects to auth login with redirectUrl of `/oauth2/login/`
* If logged in:
	* Stores `client_id`, `code_challenge`, `date`, `refresh_token` and `code` which is random generated string
	* Redirects to passed in `redirect_uri` passing back `code` and `state`
* Application callback page verifies `state` in query is same as `state` stored in session
* Calls auth `/oauth2/token/` endpoint as a `POST x-www-form-urlencoded` with form data:
	* `grant_type` - Must be "authorization_code"
	* `client_id` - The `client_id` of the application. e.g. `admin` or `cms`. This id should be registered with auth as an `oauth_client`.
	* `redirect_uri` - The url which auth will redirect to upon successful login or if already logged in. This should match the `redirect_uri` registered with auth as an `oauth_client`. e.g. `https://test-account.admin.simpleviewinc.com/oauth2/callback/`.
	* `code` - As passed in query
	* `code_verifier` - As stored in session
* An `access_token` and `refresh_token` are returned which should be stored in the client-side session
* The user is then redirected back to the original product application page

To proxy requests to GraphQL including a user's token:

* User visits product application page which runs a query
* A POST request occurs to e.g. `https://client.simpleviewapp.com/graphql/` for some query
* Application should validate is POST and if origin and/or referer headers exist they match the expected domain
* If the `access_token` is nearing expiration:
	* `POST x-www-form-urlencoded` request to auth `/oauth2/token/` route with form data:
	* `grant_type` - Must be "refresh_token"
	* `refresh_token` - This should be a valid refresh token previously obtained from auth
	* An `access_token` and `refresh_token` are returned which should be stored in the client-side session
* Proxy request to upstream `graphql.simpleviewinc.com` adding users `access_token` from client-side session as `header authorization Bearer`

The functions outlined below ease the burden of the flows described above. For Express there is a separate class which significantly eases implementation. It is the responsibility of the product application to create a valid redirect uri such as `/oauth2/callback/` in their application as well as register the client as an `oauth_client`.

#### oauth2CreateRandomKey

This function creates a random key for use as the `state` or `code_verifier`.

This function returns a string and has no parameters.

#### oauth2CreateKeyHash

This function creates a hash of the passed in string. This can be used to create a `code_challenge` from the `code_verifier` for passing to the auth login.

#### oauth2CreateLoginUrl

This function creates a login url with the required OAuth 2.0 parameters.

Returns a url to `/oauth2/login` for auth.

* args
	* `authUrl` - optional string - The URL of the auth system the website should utilise. Defaults to the live auth system. If you need to reference another system you can pass it here. Usually only used when testing in non-production environments.
	* `client_id` - string - The `client_id` of the application. e.g. `admin` or `cms`. This id should be registered with auth as an `oauth_client`.
	* `redirect_uri` - string - The url which auth will redirect to upon successful login or if already logged in. This should match the `redirect_uri` registered with auth as an `oauth_client`. e.g. `https://test-account.admin.simpleviewinc.com/oauth2/callback/`.
	* `sv_auth_params` - optional object - The properties of this object are added to the auth login url as extra parameters. This can be used to add e.g. the `account_name` or the `product`.
	* `state` - string - This is an opaque value used by the product application to maintain state between the request and callback. This value is included by auth when redirecting back to the product. This parameter should be used to prevent cross-site request forgery.
	* `code_verifier` - string - This is a random key which should be generated from `oauth2CreateRandomKey`. This key is hashed using `oauth2CreateKeyHash` and passed as a `code_challenge`.

#### oauth2CreateLogoutUrl

This function creates a logout url to log the user out of auth and then redirect back to the auth login.

* args
	* `authUrl` - optional string - The URL of the auth system the website should utilise. Defaults to the live auth system. If you need to reference another system you can pass it here. Usually only used when testing in non-production environments.
	* `redirect_uri` - string - The url which auth will redirect to upon successful login. Usually this is the current url. e.g. `https://test-account.admin.simpleviewinc.com/`.
	* `sv_auth_params` - optional object - The properties of this object are added to the auth login url as extra parameters. This can be used to add e.g. the `account_name` or the `product`.

#### oauth2GetTokens

This function calls the `/oauth2/token` route on auth. This is used for authorising the `code` in the callback.

When authorising the code the route checks that the `client_id` and the `redirect_uri` are valid for the client. The `code_verifier` is then hashed and the new hash and code are then checked that we have a previously stored `code` and `code_challenge` which is still valid. This call must be made within five minutes of the original login call which stores the challenge.

Returns a `token` and `refresh_token`.

* args
	* `authUrl` - optional string - The URL of the auth system the website should utilise. Defaults to the live auth system. If you need to reference another system you can pass it here. Usually only used when testing in non-production environments.
	* `client_id` - string - The `client_id` of the application. e.g. `admin` or `cms`. This id should be registered with auth as an `oauth_client`.
	* `redirect_uri` - string - The url which auth will redirect to upon successful login or if already logged in. This should match the `redirect_uri` registered with auth as an `oauth_client`. e.g. `https://test-account.admin.simpleviewinc.com/oauth2/callback/`.
	* `code` - string - This should have been previously passed from auth to the callback.
	* `code_verifier` - string - This is a random key which should be generated from `oauth2CreateRandomKey`. This should have been stored in the session.

#### oauth2GetTokensFromRefresh

This function calls the `/oauth2/token` route on auth. This is used for refreshing tokens.

When requesting new tokens the route checks the validity of the `refresh_token`.

Returns a token and refresh_token.

* args
	* `authUrl` - optional string - The URL of the auth system the website should utilise. Defaults to the live auth system. If you need to reference another system you can pass it here. Usually only used when testing in non-production environments.
	* `refresh_token` - string - This should be a valid refresh token previously obtained from auth.

#### Embed handoff (token-exchange)

For iframe / CHIPS SSO, a parent BFF exchanges a fresh access token for a short-lived handoff JWT, delivers it to the embed (e.g. `postMessage`), and the embed BFF redeems it for user tokens. Both calls hit `POST /oauth2/token/` with `grant_type=urn:ietf:params:oauth:grant-type:token-exchange`.

Integration contract for iframe `postMessage`, origins, and cookies: [embed-iframe-sso.md](../../embed-iframe-sso.md).

* Parent should refresh its access token before hop 1 if it is near expiry.
* Parent `oauth_client` must list the target `audience` in `token_exchange_audiences`.
* Hop 1 returns a signed `embed_handoff` JWT (~60s). Mongo stores only the JWT `jti` for single-use replay protection.
* Hop 2 redeems that JWT once for `token` + `refresh_token`.
* Export: `EMBED_HANDOFF_TOKEN_TYPE` — `urn:simpleview:params:oauth:token-type:embed_handoff` (hop 2 `subject_token_type`).

#### oauth2ExchangeToken

Hop 1 — exchange a parent access JWT for an embed handoff JWT.

Returns `{ handoff_token, expires_in }`.

* args
	* `authUrl` - optional string - The URL of the auth system. Defaults to live auth.
	* `client_id` - string - Parent oauth client id (e.g. `crm3`).
	* `subject_token` - string - Fresh parent access JWT.
	* `audience` - string - Target audience (e.g. `apex`).

#### oauth2RedeemHandoffToken

Hop 2 — redeem an embed handoff JWT for user access + refresh tokens.

Returns `{ token, refresh_token }`.

* args
	* `authUrl` - optional string - The URL of the auth system. Defaults to live auth.
	* `client_id` - string - Same parent oauth client id used in hop 1.
	* `subject_token` - string - The `handoff_token` from hop 1.
	* `audience` - string - Same audience used in hop 1.

### OAuth2Express

The OAuth2Express class provides a simple interface to allow products using Express to easily add OAuth 2.0 authentication with auth. See the [sv-auth](https://github.com/simpleviewinc/sv-auth) readme for sample usage.

* `args`
	* `authUrl` - optional string - The URL of the auth system the website should utilise. Defaults to the live auth system. If you need to reference another system you can pass it here. Usually only used when testing in non-production environments.
	* `client_id` - string - The `client_id` of the application. e.g. `admin` or `cms`. This id should be registered with auth as an `oauth_client`.
	* `callbackPath` - optional string - The path that is registered as a callback route. Defaults to `/oauth2/callback/`.
	* `logoutPath` - optional string - The path that is registered as a logout route. Defaults to `/logout/`.
	* `createParams` - optional function - When executed with `req` as a parameter, returns an object containing any parameters which are to be passed to the auth login such as `acct_id`, `account_name` and `product`. Defaults to `undefined` which will pass no extra parameters.
	* `sessionKey` - optional string - Key to store the declared session. Defaults to `session`.

#### OAuth2Express.middleware

This method is an express middleware function designed to wrap any private routes. It takes the same parameters as an Express route `(req, res, next)`.

If there is currently a user token in the session and it is older than a day, it will try to refresh the token.

If there is no user session then the user is redirected to the auth login.

If the above exceptions do not apply then the `next` method is run to move Express to the function for the route.

#### OAuth2Express.callback

This method must be registered on the callback route. It takes the same parameters as an Express route `(req, res)`.

It obtains a `token`, `refresh_token` and user from auth, stores them in the session and then redirects the user back to the original path.

#### OAuth2Express.logout

This method must be registered on the logout route. It takes the same parameters as an Express route `(req, res)`.

It deletes the session entry and redirects the user to the auth logout route, which logs the user out from auth and then redirects to the auth login.

#### OAuth2Express.applyMiddleware

This is a helper method which registers the `callback` and `logout` functions on the `callbackPath` and `logoutPath` respectively.

## Development

* Publish - `sv script sv-auth publish SEMVER`

## Related Documentation

* [sv-graphql-client](https://github.com/simpleviewinc/sv-graphql-client/)
* [sv-auth](https://github.com/simpleviewinc/sv-auth/)

## Troubleshooting

For any assistance please reach out on the [Microservices Support](https://teams.microsoft.com/l/channel/19%3A0QyLUigUKgmuRY-RfRJIVzq3y8_oURvRVvV5IC67-0w1%40thread.tacv2/General?groupId=42dd24e6-f6ae-4520-9092-cc21b3b3b730&tenantId=cc1eb3c2-fc0c-4a83-8e6a-8d886b4faeab) Teams channel.
