Skip to content

Commit

Permalink
Rework authentication based on Authentication extension, implement HT…
Browse files Browse the repository at this point in the history
…TP Basic and OpenID Connect (#439)

* Rework authentication based on STAC extension
* Add OpenID Connect support
* Fix authConfig with API key is broken #446
  • Loading branch information
m-mohr authored Jun 11, 2024
1 parent 04e5627 commit 15945d9
Show file tree
Hide file tree
Showing 34 changed files with 1,200 additions and 273 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,4 @@ The following sponsors have provided a substantial amount of funding for STAC Br
- [Matthias Mohr - Softwareentwicklung](https://mohr.ws) (maintenance)
- [Spacebel](https://spacebel.com) (collection search)
- [Planet](https://planet.com) (OpenID Connect authentication, other features, maintenance)
- [CloudFerro](https://cloudferro.com) (authentication, Alternate Asset and Storage extensions)
44 changes: 4 additions & 40 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,47 +226,11 @@
"type": [
"object"
],
"properties": {
"type": {
"type": [
"null",
"string"
],
"enum": [
"query",
"header"
]
},
"key": {
"type": [
"string"
]
},
"formatter": {
"oneOf": [
{
"type": [
"null"
],
"format": "function"
},
{
"type": [
"string"
],
"enum": [
"Bearer"
]
}
]
},
"description": {
"type": [
"string",
"null"
]
"allOf": [
{
"$ref": "https://stac-extensions.github.io/authentication/v1.1.0/schema.json"
}
},
],
"noCLI": true,
"noEnv": true
}
Expand Down
86 changes: 72 additions & 14 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,25 +288,44 @@ Please note that this option can only be provided through a config file and is n

***experimental***

This allows to enable a simple authentication form where a user can input a token, an API key or similar things.
It is disabled by default (`null`). If enabled, the token provided by the user can be used in the HTTP headers or in the query parameters of the requests. This option is affected by [`allowedDomains`](#alloweddomains).
This allows to enable some authentication methods. Currently the supported methods are:
- API Keys (`type: apiKey`) via query parameter or HTTP Header
- HTTP Basic (`type: http`, `scheme: basic`)
- OpenID Connect (`type: openIdConnect`)

There are four options you can set in the `authConfig` object:
Authentication is disabled by default (`null`).

* `type` (string): `null` (disabled), `"query"` (use token in query parameters), or `"header"` (use token in HTTP request headers).
* `key` (string): The query string parameter name or the HTTP header name respecively.
* `formatter` (function|string|null): You can optionally specify a formatter for the query string value or HTTP header value respectively. If the string `"Bearer"` is provided formats as a Bearer token according to RFC 6750. If not given, the token is provided as provided by the user.
* `description` (string|null): Optionally a description that is shown to the user. This should explain how the token can be obtained for example. CommonMark is allowed.
The options you can set in the `authConfig` object are defined in the
[Authentication Scheme Object of the STAC Authentication Extension](https://github.com/stac-extensions/authentication?tab=readme-ov-file#authentication-scheme-object) (limited by the supported methods listed above).

**Note:** Before STAC Browser 3.2.0 a different type of object was supported.
The old way is deprecated, but will be converted to the new object internally.
Please migrate to the new configuration options now.

In addition the following properties are supported:

* `formatter` (function|string|null): You can optionally specify a formatter for the query string value or HTTP header value respectively. If the string `"Bearer"` is provided formats as a Bearer token according to RFC 6750. If not given, the token is sent as provided by the user.
* `description` (string|null): Optionally a description that is shown to the user. This should explain how the credentials can be obtained for example. CommonMark is allowed.
**Note:** You can leave the description empty in the config file and instead provide a localized string with the key `authConfig` -> `description` in the file for custom phrases (`src/locales/custom.js`).

Please note that this option can only be provided through a config file and is not available via CLI/ENV.
Authentication is generally affected by the [`allowedDomains`](#alloweddomains) option.

The `authConfig` option can only be provided through a config file and is not available via CLI/ENV.

### API Keys

API keys can be configured to be sent via HTTP header or query parameter:

### Example 1: HTTP Request Header Value
- For query parameters you need to set `in: query` with a respective `name` for the query parameter
- For HTTP headers you need to set `in: header` with a respective `name` for the header field

#### Example 1: HTTP Request Header Value

```js
{
type: 'header',
key: 'Authorization',
type: 'apiKey',
in: 'header',
name: 'Authorization',
formatter: token => `Bearer ${token}`, // This is an example, there's also the simpler variant to just provide the string 'Bearer' in this case
description: `Please retrieve the token from our [API console](https://example.com/api-console).\n\nFor further questions contact <mailto:[email protected]>.`
}
Expand All @@ -315,18 +334,57 @@ Please note that this option can only be provided through a config file and is n
For a given token `123` this results in the following additional HTTP Header:
`Authorization: Bearer 123`

### Example 2: Query Parameter Value
#### Example 2: Query Parameter Value

```js
{
type: 'query',
key: 'API_KEY'
type: 'apiKey',
in: 'query',
name: 'API_KEY'
}
```

For a given token `123` this results in the following query parameter:
`https://example.com/stac/catalog.json?API_KEY=123`

### HTTP Basic

HTTP Basic is supported according to [RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617).

**Example:**

```js
{
type: 'http',
scheme: 'basic'
}
```

### OpenID Connect

**IMPORTANT: OpenID Connect is only supported if `historyMode` is set to `history`!**

For OpenID Connect some additional options must be provided, which currently follow the
[oidc-client-ts Configuration options](https://github.com/okta/okta-auth-js?tab=readme-ov-file#configuration-options).
These options (except for `issuer`) must be provided in the property `oidcConfig`.
The `clientId` option defaults to `stac-browser`.
The redirect URL for the OIDC client must be set as follows:

#### Example

```js
{
type: 'openIdConnect',
openIdConnectUrl: 'https://stac.example/.well-known/openid-configuration',
oidcOptions: {
client_id: 'abc123'
}
}
```

For a given token `123` this results in the following additional HTTP Header:
`Authorization: Bearer 123`

## preprocessSTAC

***experimental***
Expand Down
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"core-js": "^3.6.5",
"leaflet": "^1.8.0",
"node-polyfill-webpack-plugin": "^2.0.0",
"oidc-client-ts": "^3.0.1",
"remove-markdown": "^0.5.0",
"stac-layer": "^0.15.0",
"stac-node-validator": "^2.0.0-beta.7",
Expand Down
17 changes: 13 additions & 4 deletions src/StacBrowser.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<b-container id="stac-browser">
<Authentication v-if="doAuth.length > 0" />
<Authentication v-if="showLogin" />
<ErrorAlert v-if="globalError" dismissible class="global-error" v-bind="globalError" @close="hideError" />
<Sidebar v-if="sidebar" />
<!-- Header -->
Expand Down Expand Up @@ -45,6 +45,7 @@ import URI from 'urijs';
import { API_LANGUAGE_CONFORMANCE } from './i18n';
import { getBest, prepareSupported } from './locale-id';
import BrowserStorage from "./browser-store";
import Authentication from "./components/Authentication.vue";
Vue.use(AlertPlugin);
Vue.use(ButtonGroupPlugin);
Expand Down Expand Up @@ -93,7 +94,7 @@ export default {
router,
store,
components: {
Authentication: () => import('./components/Authentication.vue'),
Authentication,
ErrorAlert,
Sidebar: () => import('./components/Sidebar.vue'),
StacHeader
Expand All @@ -109,13 +110,14 @@ export default {
};
},
computed: {
...mapState(['allowSelectCatalog', 'data', 'dataLanguage', 'description', 'doAuth', 'globalError', 'stateQueryParameters', 'title', 'uiLanguage', 'url']),
...mapState(['allowSelectCatalog', 'data', 'dataLanguage', 'description', 'globalError', 'stateQueryParameters', 'title', 'uiLanguage', 'url']),
...mapState({
detectLocaleFromBrowserFromVueX: 'detectLocaleFromBrowser',
supportedLocalesFromVueX: 'supportedLocales',
storeLocaleFromVueX: 'storeLocale'
}),
...mapGetters(['displayCatalogTitle', 'fromBrowserPath', 'isExternalUrl', 'root', 'supportsConformance', 'toBrowserPath']),
...mapGetters('auth', ['showLogin']),
browserVersion() {
if (typeof STAC_BROWSER_VERSION !== 'undefined') {
return STAC_BROWSER_VERSION;
Expand Down Expand Up @@ -245,7 +247,7 @@ export default {
}
}
},
created() {
async created() {
this.$router.onReady(() => {
this.detectLocale();
this.parseQuery(this.$route);
Expand All @@ -268,6 +270,13 @@ export default {
this.$store.commit(resetOp);
this.parseQuery(to);
});
const storage = new BrowserStorage(true);
const authConfig = storage.get('authConfig');
if (authConfig) {
storage.remove('authConfig');
await this.$store.dispatch('config', { authConfig });
}
},
mounted() {
this.$root.$on('error', this.showError);
Expand Down
59 changes: 59 additions & 0 deletions src/auth/apiKey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Auth from "./index";
import i18n from '../i18n';
import Utils from "../utils";

export default class ApiKey extends Auth {

constructor(options, changeListener, router) {
super(options, changeListener, router);
}

getButtonTitle() {
return i18n.t('authentication.button.title');
}

getComponent() {
return 'ApiKey';
}

getComponentProps() {
return {
description: this.options.description
};
}

async logout(/*credentials*/) {
if (this.router.currentRoute.name !== 'logout') {
this.router.push('/auth/logout');
}
return true;
}

updateStore(value) {
if (value) {
if (this.options.formatter === 'Bearer') {
value = `Bearer ${value}`;
}
else if (typeof this.options.formatter === 'function') {
value = this.options.formatter(value);
}
}
if (!Utils.hasText(value)) {
value = undefined;
}

// Set query or request parameters
let key = this.options.name;
if (this.options.in === 'query') {
return {
query: { type: 'private', key, value }
};
}
else if (this.options.in === 'header') {
return {
header: { key, value }
};
}
}

}
44 changes: 44 additions & 0 deletions src/auth/basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Auth from "./index";
import i18n from '../i18n';
import Utils from "../utils";

export default class BasicAuth extends Auth {

constructor(options, changeListener, router) {
super(options, changeListener, router);
}

getComponent() {
return 'Basic';
}

getComponentProps() {
return {
description: this.options.description
};
}

getButtonTitle() {
return i18n.t('authentication.button.title');
}

async logout(/*credentials*/) {
if (this.router.currentRoute.name !== 'logout') {
this.router.push('/auth/logout');
}
return true;
}

updateStore(value) {
if (typeof value === 'string' && value.length >= 3) {
value = `Basic ${btoa(value)}`;
}
if (!Utils.hasText(value)) {
value = undefined;
}
return {
header: { key: 'Authorization', value }
};
}

}
Loading

0 comments on commit 15945d9

Please sign in to comment.