diff --git a/packages/browser/test/index.test.ts b/packages/browser/test/index.test.ts
index decb1a59..5191da31 100644
--- a/packages/browser/test/index.test.ts
+++ b/packages/browser/test/index.test.ts
@@ -13,10 +13,11 @@ describe('nile db', () => {
if (k === 'auth') {
expect(props).toEqual([
'constructor',
+ 'createProvider',
'getSSOProviders',
'login',
'signUp',
- 'updateSSOProvider',
+ 'updateProvider',
]);
}
if (k === 'users') {
diff --git a/packages/react/src/SSO/BaseSSOForm.tsx b/packages/react/src/SSO/BaseSSOForm.tsx
index 4f45f09e..cec91e40 100644
--- a/packages/react/src/SSO/BaseSSOForm.tsx
+++ b/packages/react/src/SSO/BaseSSOForm.tsx
@@ -1,14 +1,16 @@
import React from 'react';
import { useMutation } from '@tanstack/react-query';
-import { UpdateSSOProviderRequest } from '@theniledev/browser';
+import { UpdateProviderRequest } from '@theniledev/browser';
import SimpleForm from '../lib/SimpleForm';
import { useApi } from '../context';
import { Attribute, AttributeType } from '../lib/SimpleForm/types';
-import { Props } from './types';
+import { OktaProps } from './types';
-export default function BaseSSOForm(props: Props & { providerName: string }) {
+export default function BaseSSOForm(
+ props: OktaProps & { providerName: string }
+) {
const api = useApi();
const { config, providerName, onSuccess, onError, allowEdit = true } = props;
const attributes = React.useMemo(() => {
@@ -26,6 +28,8 @@ export default function BaseSSOForm(props: Props & { providerName: string }) {
label: 'Config url',
type: AttributeType.Text,
defaultValue: config?.configUrl ?? '',
+ helpText:
+ 'The URL of the .well-known/openid-configuration for the identity provider',
required: true,
disabled: !allowEdit,
},
@@ -33,7 +37,8 @@ export default function BaseSSOForm(props: Props & { providerName: string }) {
name: 'redirectURI',
label: 'Redirect URI',
type: AttributeType.Text,
- helpText: 'Where users should be redirected to upon login',
+ helpText:
+ 'Where users should be redirected to after a successful login',
defaultValue: config?.redirectURI ?? '',
required: true,
disabled: !allowEdit,
@@ -44,7 +49,8 @@ export default function BaseSSOForm(props: Props & { providerName: string }) {
type: AttributeType.Text,
defaultValue: config?.emailDomains?.join(', ') ?? '',
required: true,
- helpText: 'A comma seperated list of email domains to be used',
+ helpText:
+ 'A comma seperated list of email domains (@yourDomain.com) to be used',
disabled: !allowEdit,
},
];
@@ -68,11 +74,19 @@ export default function BaseSSOForm(props: Props & { providerName: string }) {
]);
const mutation = useMutation(
- (ssoRequest: UpdateSSOProviderRequest) => {
- return api.auth.updateSSOProvider({
+ (ssoRequest: UpdateProviderRequest & { emailDomains: string }) => {
+ const payload = {
providerName: providerName.toLowerCase(),
- updateSSOProviderRequest: ssoRequest,
- });
+ updateProviderRequest: {
+ ...ssoRequest,
+ emailDomains: ssoRequest.emailDomains.split(','),
+ },
+ };
+ if (config != null) {
+ return api.auth.updateProvider(payload);
+ } else {
+ return api.auth.createProvider(payload);
+ }
},
{
onSuccess,
diff --git a/packages/react/src/SSO/Okta.tsx b/packages/react/src/SSO/Okta.tsx
index b59bc8dc..c97ab75e 100644
--- a/packages/react/src/SSO/Okta.tsx
+++ b/packages/react/src/SSO/Okta.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import BaseSSOForm from './BaseSSOForm';
-import { Props } from './types';
+import { OktaProps } from './types';
-export default function Okta(props: Props) {
+export default function Okta(props: OktaProps) {
return ;
}
diff --git a/packages/react/src/SSO/index.ts b/packages/react/src/SSO/index.ts
index 98402daa..adfa6200 100644
--- a/packages/react/src/SSO/index.ts
+++ b/packages/react/src/SSO/index.ts
@@ -1,2 +1,3 @@
export { default as Okta } from './Okta';
export { default } from './BaseSSOForm';
+export * from './types';
diff --git a/packages/react/src/SSO/types.ts b/packages/react/src/SSO/types.ts
index c8612035..bce5c92c 100644
--- a/packages/react/src/SSO/types.ts
+++ b/packages/react/src/SSO/types.ts
@@ -1,7 +1,7 @@
-import { GetSSOProvider200Response } from '@theniledev/browser';
+import { GetSSOProviders200Response } from '@theniledev/browser';
-export type Props = {
- config?: GetSSOProvider200Response;
+export type OktaProps = {
+ config?: GetSSOProviders200Response;
onSuccess?: (data: unknown, variables: unknown) => void;
onError?: (e: Error) => void;
allowEdit?: boolean;
diff --git a/packages/react/test/SSO/Okta.test.tsx b/packages/react/test/SSO/Okta.test.tsx
index b0f583a1..eebade88 100644
--- a/packages/react/test/SSO/Okta.test.tsx
+++ b/packages/react/test/SSO/Okta.test.tsx
@@ -13,7 +13,7 @@ describe('Okta', () => {
global.fetch = token;
const api = {
auth: {
- updateSSOProvider: async () => jest.fn(),
+ createProvider: async () => jest.fn(),
},
} as unknown as Client;
render(
diff --git a/packages/server/openapi/index.json b/packages/server/openapi/index.json
index bdbb584b..55b04cc8 100644
--- a/packages/server/openapi/index.json
+++ b/packages/server/openapi/index.json
@@ -37,6 +37,9 @@
},
"put": {
"$ref": "../src/auth/providers/openapi/paths/updateProvider.json"
+ },
+ "post": {
+ "$ref": "../src/auth/providers/openapi/paths/createProvider.json"
}
}
},
diff --git a/packages/server/openapi/spec.json b/packages/server/openapi/spec.json
index 41a6d2a4..8bd32916 100644
--- a/packages/server/openapi/spec.json
+++ b/packages/server/openapi/spec.json
@@ -990,7 +990,252 @@
],
"summary": "Update SSO provider",
"description": "Update SSO provider by name",
- "operationId": "updateSSOProvider",
+ "operationId": "updateProvider",
+ "parameters": [
+ {
+ "name": "providerName",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "*/*": {
+ "schema": {
+ "required": [
+ "clientId",
+ "clientSecret",
+ "configUrl",
+ "emailDomains",
+ "redirectURI"
+ ],
+ "type": "object",
+ "properties": {
+ "configUrl": {
+ "type": "string",
+ "format": "uri"
+ },
+ "clientId": {
+ "type": "string"
+ },
+ "clientSecret": {
+ "type": "string"
+ },
+ "redirectURI": {
+ "type": "string",
+ "format": "uri"
+ },
+ "emailDomains": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "enabled": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Updated OIDC provider",
+ "content": {
+ "application/json": {
+ "schema": {
+ "required": [
+ "clientId",
+ "configUrl",
+ "provider",
+ "redirectURI",
+ "tenantId"
+ ],
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "readOnly": true
+ },
+ "tenantId": {
+ "type": "string"
+ },
+ "provider": {
+ "type": "string"
+ },
+ "configUrl": {
+ "type": "string",
+ "format": "uri"
+ },
+ "clientId": {
+ "type": "string"
+ },
+ "redirectURI": {
+ "type": "string",
+ "format": "uri"
+ },
+ "emailDomains": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "enabled": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "OIDC provider name mismatch",
+ "content": {
+ "application/json": {
+ "schema": {
+ "required": [
+ "errorCode",
+ "message",
+ "statusCode"
+ ],
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "errorCode": {
+ "type": "string",
+ "enum": [
+ "internal_error",
+ "bad_request",
+ "entity_not_found",
+ "duplicate_entity",
+ "invalid_credentials",
+ "unknown_oidc_provider",
+ "provider_already_exists",
+ "provider_config_error",
+ "provider_mismatch",
+ "provider_update_error",
+ "session_state_missing",
+ "session_state_mismatch",
+ "oidc_code_missing"
+ ],
+ "message": {
+ "type": "string"
+ },
+ "statusCode": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "required": [
+ "errorCode",
+ "message",
+ "statusCode"
+ ],
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "errorCode": {
+ "type": "string",
+ "enum": [
+ "internal_error",
+ "bad_request",
+ "entity_not_found",
+ "duplicate_entity",
+ "invalid_credentials",
+ "unknown_oidc_provider",
+ "provider_already_exists",
+ "provider_config_error",
+ "provider_mismatch",
+ "provider_update_error",
+ "session_state_missing",
+ "session_state_mismatch",
+ "oidc_code_missing"
+ ],
+ "message": {
+ "type": "string"
+ },
+ "statusCode": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "OIDC provider not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "required": [
+ "errorCode",
+ "message",
+ "statusCode"
+ ],
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "errorCode": {
+ "type": "string",
+ "enum": [
+ "internal_error",
+ "bad_request",
+ "entity_not_found",
+ "duplicate_entity",
+ "invalid_credentials",
+ "unknown_oidc_provider",
+ "provider_already_exists",
+ "provider_config_error",
+ "provider_mismatch",
+ "provider_update_error",
+ "session_state_missing",
+ "session_state_mismatch",
+ "oidc_code_missing"
+ ],
+ "message": {
+ "type": "string"
+ },
+ "statusCode": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "jwtBearerAuth": []
+ }
+ ]
+ },
+ "post": {
+ "tags": [
+ "authentication"
+ ],
+ "summary": "Create SSO provider",
+ "description": "Create an SSO provider by name",
+ "operationId": "createProvider",
"parameters": [
{
"name": "providerName",
diff --git a/packages/server/src/auth/auth.test.ts b/packages/server/src/auth/auth.test.ts
index 3bd64ecf..8113d6c2 100644
--- a/packages/server/src/auth/auth.test.ts
+++ b/packages/server/src/auth/auth.test.ts
@@ -5,10 +5,12 @@ import Auth from './';
const baseConfig = [
'_tenantId',
'api',
+ 'createProvider',
'database',
'workspace',
'db',
- 'getProviders',
+ 'listProviders',
+ 'listTenantProviders',
'login',
'loginSSO',
'loginSSOUrl',
diff --git a/packages/server/src/auth/index.ts b/packages/server/src/auth/index.ts
index b82d8bd1..f7b7c4ea 100644
--- a/packages/server/src/auth/index.ts
+++ b/packages/server/src/auth/index.ts
@@ -30,7 +30,7 @@ export default class Auth extends Config {
const sso = params.get('sso');
if (sso) {
- const providerRes = await this.getProviders(
+ const providerRes = await this.listProviders(
(req as Request).clone(),
init
);
@@ -49,13 +49,13 @@ export default class Auth extends Config {
// is there a way to do this? probably not.
headers.set(X_NILE_TENANT, providers[0].tenantId);
- const ssoResp = await new Response(
- (
- await this.loginSSO(req)
- ).body
- ).json();
- const redirectUrl = ssoResp.uri;
- return Response.redirect(redirectUrl, 302);
+ headers.append(
+ 'set-cookie',
+ `tenantId=${providers[0].tenantId}; path=/; httponly;`
+ );
+ await this.loginSSO(req);
+ // make it a client side redirect, because of the headers
+ // return Response.redirect(redirectUrl, 302);
// if there is no provider, require a password.
}
}
@@ -69,18 +69,16 @@ export default class Auth extends Config {
if (res instanceof ResponseError) {
return res.response;
}
-
if (res && res.status >= 200 && res.status < 300) {
const token: RestModels.LoginUserResponse = await res.json();
const cookie = `${this.api?.cookieKey}=${token.token.jwt}; path=/; samesite=lax; httponly;`;
- headers.set('set-cookie', cookie);
- const hasTenantId = headers.get(X_NILE_TENANT);
- if (!hasTenantId) {
- const { tenants } = token;
- const tenant = tenants?.values();
- const tenantId = tenant?.next().value;
- headers.set(X_NILE_TENANT, tenantId);
- }
+ headers.append('set-cookie', cookie);
+ // const hasTenantId = headers.get(X_NILE_TENANT);
+ const { tenants } = token;
+ const tenant = tenants?.values();
+ const tenantId = tenant?.next().value;
+ headers.set(X_NILE_TENANT, tenantId);
+ headers.append('set-cookie', `tenantId=${tenantId}; path=/; httponly;`);
return new Response(JSON.stringify(token), { status: 200, headers });
}
const text = await res.text();
@@ -125,7 +123,23 @@ export default class Auth extends Config {
}/auth/oidc/providers/${encodeURIComponent(providerName)}`;
}
- updateProvider = async (
+ get listTenantProvidersUrl() {
+ return `/workspaces/${encodeURIComponent(
+ this.workspace
+ )}/databases/${encodeURIComponent(this.database)}/tenants/${
+ this.tenantId ? encodeURIComponent(this.tenantId) : '{tenantId}'
+ }/auth/oidc/providers`;
+ }
+
+ listTenantProviders = async (
+ req: NileRequest,
+ init?: RequestInit
+ ): NileResponse => {
+ const _requester = new Requester(this);
+ return _requester.get(req, this.listTenantProvidersUrl, init);
+ };
+
+ createProvider = async (
req: NileRequest,
init?: RequestInit
): NileResponse => {
@@ -134,6 +148,15 @@ export default class Auth extends Config {
return _requester.post(req, this.updateProviderUrl(providerName), init);
};
+ updateProvider = async (
+ req: NileRequest,
+ init?: RequestInit
+ ): NileResponse => {
+ const _requester = new Requester(this);
+ const providerName = 'okta';
+ return _requester.put(req, this.updateProviderUrl(providerName), init);
+ };
+
providerUrl(email?: undefined | string) {
return `/workspaces/${encodeURIComponent(
this.workspace
@@ -143,10 +166,11 @@ export default class Auth extends Config {
email ? `?email=${encodeURIComponent(email)}` : ''
}`;
}
- getProviders = async (
+
+ listProviders = async (
req: NileRequest,
init?: RequestInit
- ): NileResponse => {
+ ): NileResponse => {
const _requester = new Requester(this);
let body: { email: string } | undefined;
// this is a get. Get the email from the response body so the request is filtered.
diff --git a/packages/server/src/auth/login/login.test.ts b/packages/server/src/auth/login/login.test.ts
index 2046d044..ea67416e 100644
--- a/packages/server/src/auth/login/login.test.ts
+++ b/packages/server/src/auth/login/login.test.ts
@@ -13,14 +13,27 @@ describe('login', () => {
global.Response = FakeResponse;
//@ts-expect-error - test
global.Request = FakeRequest;
- global.fetch = _fetch({ token: { jwt: 'adfasdfdsa' } });
+ global.fetch = _fetch({
+ token: { jwt: 'adfasdfdsa' },
+ tenants: {
+ values: () => {
+ return {
+ next: () => ({
+ value: 'adfdsafdsf',
+ }),
+ };
+ },
+ },
+ });
const _config = new Config(config);
const { login } = new Auth(_config);
const params = { email: 'email', password: 'password' };
const resp = await login(params);
const headers = new Headers(resp.headers);
const cookie = headers.get('set-cookie');
- expect(cookie).toEqual('token=adfasdfdsa; path=/; samesite=lax; httponly;');
+ expect(cookie).toEqual(
+ 'token=adfasdfdsa; path=/; samesite=lax; httponly;, tenantId=adfdsafdsf; path=/; httponly;'
+ );
});
it('goes to the right url', () => {
diff --git a/packages/server/src/auth/providers/openapi/paths/createProvider.json b/packages/server/src/auth/providers/openapi/paths/createProvider.json
new file mode 100644
index 00000000..9e933e1d
--- /dev/null
+++ b/packages/server/src/auth/providers/openapi/paths/createProvider.json
@@ -0,0 +1,73 @@
+{
+ "tags": ["authentication"],
+ "summary": "Create SSO provider",
+ "description": "Create an SSO provider by name",
+ "operationId": "createProvider",
+ "parameters": [
+ {
+ "name": "providerName",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "*/*": {
+ "schema": {
+ "$ref": "../../../../../openapi/index.json#/components/schemas/RegisterSSO"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Updated OIDC provider",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../../../../../openapi/index.json#/components/schemas/SSOProvider"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "OIDC provider name mismatch",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../../../../../openapi/index.json#/components/schemas/APIError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../../../../../openapi/index.json#/components/schemas/APIError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "OIDC provider not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../../../../../openapi/index.json#/components/schemas/APIError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "jwtBearerAuth": []
+ }
+ ]
+}
diff --git a/packages/server/src/auth/providers/openapi/paths/updateProvider.json b/packages/server/src/auth/providers/openapi/paths/updateProvider.json
index 630396f1..f509ad92 100644
--- a/packages/server/src/auth/providers/openapi/paths/updateProvider.json
+++ b/packages/server/src/auth/providers/openapi/paths/updateProvider.json
@@ -2,7 +2,7 @@
"tags": ["authentication"],
"summary": "Update SSO provider",
"description": "Update SSO provider by name",
- "operationId": "updateSSOProvider",
+ "operationId": "updateProvider",
"parameters": [
{
"name": "providerName",
diff --git a/packages/server/src/auth/providers/providers.test.ts b/packages/server/src/auth/providers/providers.test.ts
index 13b38f3b..4824b30b 100644
--- a/packages/server/src/auth/providers/providers.test.ts
+++ b/packages/server/src/auth/providers/providers.test.ts
@@ -11,7 +11,7 @@ const config = {
database: 'database',
tenantId: 'tenantId',
};
-describe('getProviders', () => {
+describe('listProviders', () => {
it('does a get', async () => {
//@ts-expect-error - test
global.Response = FakeResponse;
@@ -19,9 +19,9 @@ describe('getProviders', () => {
global.Request = FakeRequest;
global.fetch = _fetch();
const _config = new Config(config);
- const { getProviders } = new Auth(_config);
+ const { listProviders } = new Auth(_config);
- const res = await getProviders();
+ const res = await listProviders();
//@ts-expect-error - test
expect(res.config).toEqual(
_config.api.basePath +
diff --git a/packages/server/src/utils/Requester/index.ts b/packages/server/src/utils/Requester/index.ts
index 7b9071f4..b66245d2 100644
--- a/packages/server/src/utils/Requester/index.ts
+++ b/packages/server/src/utils/Requester/index.ts
@@ -55,16 +55,24 @@ export default class Requester extends Config {
const headers = new Headers(init ? init?.headers : {});
if (req instanceof Headers) {
const tenantId = req.get(X_NILE_TENANT);
+ const cookie = req.get('cookie');
if (tenantId) {
headers.set(X_NILE_TENANT, tenantId);
}
+ if (cookie) {
+ headers.set('cookie', cookie);
+ }
} else if (req instanceof Request) {
// pass back the X_NILE_TENANT
const _headers = new Headers(req?.headers);
const tenantId = _headers.get(X_NILE_TENANT);
+ const cookie = _headers.get('cookie');
if (tenantId) {
headers.set(X_NILE_TENANT, tenantId);
}
+ if (cookie) {
+ headers.set('cookie', cookie);
+ }
}
// default the body - may be the actual payload for the API
let body: string | undefined = JSON.stringify(req);
diff --git a/packages/server/src/utils/fetch.ts b/packages/server/src/utils/fetch.ts
index d98813e6..3aa65d4b 100644
--- a/packages/server/src/utils/fetch.ts
+++ b/packages/server/src/utils/fetch.ts
@@ -54,18 +54,17 @@ export async function _fetch(
basicHeaders.set('content-type', 'application/json; charset=utf-8');
const authHeader = headers.get('Authorization');
if (!authHeader) {
- if (config.api?.token) {
- basicHeaders.set('Authorization', `Bearer ${config.api?.token}`);
+ const token = getTokenFromCookie(headers, cookieKey);
+ if (token) {
+ basicHeaders.set('Authorization', `Bearer ${token}`);
} else {
- const token = getTokenFromCookie(headers, cookieKey);
-
- if (token) {
- basicHeaders.set('Authorization', `Bearer ${token}`);
- }
+ basicHeaders.set('Authorization', `Bearer ${config.api?.token}`);
}
}
- const tenantId = config.tenantId ?? headers?.get(X_NILE_TENANT);
+ const cookieTenant = getTokenFromCookie(headers, 'tenantId');
+ const tenantId =
+ cookieTenant ?? headers?.get(X_NILE_TENANT) ?? config.tenantId;
updateTenantId(tenantId);
if (url.includes('{tenantId}') && !tenantId) {
diff --git a/packages/server/test/fetch.mock.ts b/packages/server/test/fetch.mock.ts
index 5f6176ca..0f36ff67 100644
--- a/packages/server/test/fetch.mock.ts
+++ b/packages/server/test/fetch.mock.ts
@@ -5,22 +5,27 @@ type Something = any;
export class FakeResponse {
[key: string]: Something;
- payload: string;
+ payload: object | string;
headers?: Headers;
- constructor(payload: string, config?: RequestInit) {
+ constructor(payload: object | string, config?: RequestInit) {
this.payload = payload;
if (config) {
this.headers = new Headers(config.headers);
}
+ let pload = payload;
if (typeof payload === 'string') {
- const pload = JSON.parse(payload);
- Object.keys(pload).map((key) => {
- this[key] = pload[key];
- });
+ pload = JSON.parse(payload);
}
+
+ Object.keys(pload).map((key) => {
+ this[key] = (pload as Record)[key];
+ });
}
json = async () => {
- return JSON.parse(this.payload);
+ if (typeof this.payload === 'string') {
+ return JSON.parse(this.payload);
+ }
+ return this.payload;
};
text = async () => {
return this.payload;
@@ -42,13 +47,11 @@ export class FakeRequest {
export const _fetch = (payload?: Record) =>
(async (config: Config, path: string, opts?: RequestInit) => {
- return new FakeResponse(
- JSON.stringify({
- ...payload,
- config,
- path,
- opts,
- status: 200,
- })
- );
+ return new FakeResponse({
+ ...payload,
+ config,
+ path,
+ opts,
+ status: 200,
+ });
}) as unknown as typeof fetch;