Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- In `apps/web` workspace, create a string first in `apps/web/config/strings.ts` and then import it in the `.tsx` files, instead of using inline strings.
- When working with forms, always use refs to keep the current state of the form's data and use it to enable/disable the form submit button.
- Check the name field inside each package's package.json to confirm the right name—skip the top-level one.
- While working with forms, always use zod and react-hook-form to validate the form. Take reference implementation from `apps/web/components/admin/settings/sso/new.tsx`.

## Testing instructions

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/docs/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export const SIDEBAR: Sidebar = {
{ text: "Create a school", link: "en/schools/create" },
{ text: "Use custom domain", link: "en/schools/add-custom-domain" },
{ text: "Set up payments", link: "en/schools/set-up-payments" },
{ text: "Single Sign-On", link: "en/schools/sso" },
{ text: "Delete a school", link: "en/schools/delete" },
],
Users: [
Expand Down
117 changes: 117 additions & 0 deletions apps/docs/src/pages/en/schools/sso.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
title: Set up Single Sign On (SSO)
description: Learn how to set up Single Sign On (SSO)
layout: ../../../layouts/MainLayout.astro
---

Using SSO, you can authenticate users with their existing accounts on platforms like [Okta](https://www.okta.com/), [OneLogin](https://www.onelogin.com/), [Azure AD](https://azure.microsoft.com/en-us/services/active-directory/), etc.

> The feature is currently in alpha, which means you may encounter bugs. Please report them in our <a href="https://discord.com/invite/GR4bQsN" target="_blank">Discord</a> group if you run into any.

To use this feature on [courseLit.app](https://courselit.app), you need to be on the Enterprise plan. For self-hosted instances, this feature is available by default.

## Steps to set up SSO

1. Subscribe to the [Enterprise](https://app.courselit.app/account/billing) plan, if you haven't, to unlock the feature. Ignore this step for self-hosted instances.

2. In the CourseLit dashboard, go to `Settings` -> `Miscellaneous` -> `Login providers`.

![Login providers area](/assets/schools/login-providers-area.png)

3. Click on the Cog icon next to the SSO provider to open SSO configuration.

![SSO configuration button](/assets/schools/sso-configure-icon.png)

4. In the `SSO Provider` screen, use the `School Settings` to configure your IdP provider. Refer to the sections below to see how to configure your IdP provider.

The following is a description of the fields under this panel:

- **SAML ACS URL**: This is the URL that your IdP will send the SAML response to. This is usually `https://<school>.courselit.app/api/auth/sso/saml2/sp/acs/sso`
- **Audience URI (SP Entity ID)**: This is the URL that your IdP will use to validate the SAML response. This is usually `https://<school>.courselit.app/api/auth/sso/saml2/sp/metadata?providerId=sso`

5. After configuring the IdP provider, obtain the required settings from it and populate the values in the `IDP Configuration` panel.

The following is a description of the fields under this panel:

- **Entry point**: This is the URL CourseLit will use to send the SAML request to your IdP.
- **Certificate**: This is the certificate that your IdP will use to validate the SAML response.
- **IDP Metadata**: This is the metadata that your IdP will use to validate the SAML response.

Here is an example configuration for Okta:

![Okta SSO Configuration](/assets/schools/sso-idp-configuration-example.png)

6. Click on the `Save` button to save the configuration.

![SSO Configuration save button](/assets/schools/sso-save-config-button.png)

7. Go back to the `Login providers` screen and enable the SSO provider.

![Enable SSO provider](/assets/schools/sso-enable-checkbox.png)

## Setup IdP

### Okta

1. Go to Okta dashboard and click on `Applications` -> `Applications`.
2. Click on `Create App Integration`.
3. Select `SAML 2.0` on the `Sign-in method` popup and click on `Next`.
4. On the `Create SAML Integration` screen, in the `General Settings` tab, enter `App name` and click on `Next`.
5. In the `Configure SAML` tab, enter the `SAML ACS URL` (obtained from CourseLit) in the `Single sign-on URL` field and `Audience URI (SP Entity ID)` (obtained from CourseLit) in the `Audience URI (SP Entity ID)` field and click on `Next`.
6. In the `Feedback` tab, select the `internal app` option and click on `Finish`.
7. You will be taken to the newly created app's settings. Your Okta IdP is now configured.
8. Next, let's obtain the `Entry point`, `IdP metadata` and `Certificate` from Okta. From the `Sign On` tab, obtain the following:
<br />
<br />

- **Entry point**: We can infer this from the Metadata URL. It is usually `https://<okta-account>.okta.com/app/<okta-app-id>/sso/saml2`

![Okta entry point](/assets/schools/idp/okta/entry-point.png)
<br />
<br />

- **IdP metadata** and **Certificate**:
To obtain these, scroll down on the same page and locate the `SAML Signing Certificates` section. Click on the `Actions` button next to the `SHA-2` and copy the IdP metadata and download the certificate.

![Okta IdP metadata](/assets/schools/idp/okta/saml-signing-certificates.png)

9. Enter the values obtained in the `IDP Configuration` panel.
10. The Okta IdP is now configured.

## Customer's experience

When the SSO login provider is configured and enabled, the customer will see a `Login with SSO` button on the login page and checkout page.

### 1. Login page

![SSO login view](/assets/schools/sso-login-view.png)

### 2. Checkout page

![SSO checkout view](/assets/schools/sso-checkout-view.png)

## Troubleshooting

### 1. Email login is disabled and now I am locked out

#### a. Cloud-hosted (courselit.app)

You can re-enable the email provider from the [CourseLit](https://app.courselit.app) dashboard.

![Re-enable email login provider](/assets/schools/reenable-email-login-provider.png)

#### b. Self-hosted

You need to log in to your school's MongoDB instance and run the following query to re-enable the email provider:

```javascript
db.domains.updateMany({}, { $addToSet: { "settings.logins": "email" } });
```

### 2. Can I add multiple SSO providers?

Since this feature is currently in alpha, you can only add one SSO provider at a time. We want to make sure that the feature is stable before adding more providers.

## Stuck somewhere?

We are always here for you. Come chat with us in our <a href="https://discord.com/invite/GR4bQsN" target="_blank">Discord</a> channel or send a tweet at <a href="https://twitter.com/courselit" target="_blank">@CourseLit</a>.
28 changes: 28 additions & 0 deletions apps/web/.migrations/13-12-25_23-07-init-login-settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import mongoose from "mongoose";

mongoose.connect(process.env.DB_CONNECTION_STRING, {
useNewUrlParser: true,
useUnifiedTopology: true,
});

const SettingsSchema = new mongoose.Schema({
logins: { type: [String] },
});

const DomainSchema = new mongoose.Schema({
name: { type: String, required: true, unique: true },
settings: SettingsSchema,
});

const Domain = mongoose.model("Domain", DomainSchema);

const addEmailLoginToDomainSettings = async () => {
console.log("🏁 Migrating login settings");
await Domain.updateMany({}, { $set: { "settings.logins": ["email"] } });
console.log("🏁 Migrated login settings");
};

(async () => {
await addEmailLoginToDomainSettings();
mongoose.connection.close();
})();
20 changes: 20 additions & 0 deletions apps/web/app/(with-contexts)/(with-layout)/checkout/product.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import { AddressContext } from "@components/contexts";
import Checkout, { Product } from "@components/public/payments/checkout";
import { Constants, PaymentPlan, Course } from "@courselit/common-models";
import type { MembershipEntityType } from "@courselit/common-models";
import { useToast } from "@courselit/components-library";
import { FetchBuilder } from "@courselit/utils";
import { TOAST_TITLE_ERROR } from "@ui-config/strings";
import { useSearchParams } from "next/navigation";
import { useCallback, useContext, useEffect, useState } from "react";
import type { SSOProvider } from "../login/page";

const { MembershipEntityType } = Constants;

Expand All @@ -21,6 +23,7 @@ export default function ProductCheckout() {
const [product, setProduct] = useState<Product | null>(null);
const [paymentPlans, setPaymentPlans] = useState<PaymentPlan[]>([]);
const [includedProducts, setIncludedProducts] = useState<Course[]>([]);
const [ssoProvider, setSSOProvider] = useState<SSOProvider | undefined>();

const getIncludedProducts = useCallback(async () => {
const query = `
Expand Down Expand Up @@ -94,6 +97,10 @@ export default function ProductCheckout() {
}
defaultPaymentPlan
}
ssoProvider: getSSOProvider {
providerId
domain
}
}
`;
const fetch = new FetchBuilder()
Expand All @@ -119,6 +126,9 @@ export default function ProductCheckout() {
description: "Course not found",
});
}
if (response.ssoProvider) {
setSSOProvider(response.ssoProvider);
}
} catch (err: any) {
toast({
title: TOAST_TITLE_ERROR,
Expand Down Expand Up @@ -154,6 +164,10 @@ export default function ProductCheckout() {
joiningReasonText
defaultPaymentPlan
}
ssoProvider: getSSOProvider {
providerId
domain
}
}
`;
const fetch = new FetchBuilder()
Expand All @@ -180,6 +194,9 @@ export default function ProductCheckout() {
description: "Community not found",
});
}
if (response.ssoProvider) {
setSSOProvider(response.ssoProvider);
}
} catch (err: any) {
toast({
title: TOAST_TITLE_ERROR,
Expand Down Expand Up @@ -214,6 +231,9 @@ export default function ProductCheckout() {
product={product}
paymentPlans={paymentPlans}
includedProducts={includedProducts}
ssoProvider={ssoProvider}
type={entityType as MembershipEntityType | undefined}
id={entityId as string | undefined}
/>
);
}
Loading
Loading