diff --git a/astro/.astro/types.d.ts b/astro/.astro/types.d.ts index 27eca387d2..7c91579fbd 100644 --- a/astro/.astro/types.d.ts +++ b/astro/.astro/types.d.ts @@ -118,6 +118,13 @@ declare module 'astro:content' { collection: "articles", data: InferEntrySchema<"articles"> } & { render(): Render[".md"] }, +"authentication/how-sso-works.mdx": { + id: "authentication/how-sso-works.mdx", + slug: "authentication/how-sso-works", + body: string, + collection: "articles", + data: InferEntrySchema<"articles"> +} & { render(): Render[".mdx"] }, "authentication/index.mdx": { id: "authentication/index.mdx", slug: "authentication", @@ -757,6 +764,22 @@ declare module 'astro:content' { collection: "dev-tools", data: InferEntrySchema<"dev-tools"> } & { render(): Render[".mdx"] }, +}, +"quickstarts": { +"index.mdx": { + id: "index.mdx", + slug: "index", + body: string, + collection: "quickstarts", + data: InferEntrySchema<"quickstarts"> +} & { render(): Render[".mdx"] }, +"quickstart-ruby-rails-web.mdx": { + id: "quickstart-ruby-rails-web.mdx", + slug: "quickstart-ruby-rails-web", + body: string, + collection: "quickstarts", + data: InferEntrySchema<"quickstarts"> +} & { render(): Render[".mdx"] }, }, }; diff --git a/astro/src/components/RemoteCode.astro b/astro/src/components/RemoteCode.astro new file mode 100644 index 0000000000..70b9df3d40 --- /dev/null +++ b/astro/src/components/RemoteCode.astro @@ -0,0 +1,66 @@ +--- +/** + * Remote code fetcher and syntax highlighter via the Astro Code component. + */ + +export interface Props { + /** + * Url to fetch code from. Also allows selecting specific line(s) of code with + * the Github line syntax. + * E.g., https://raw.githubusercontent.com/FusionAuth/fusionauth-example-client-libraries/main/ruby/Gemfile#L3 + */ + url: string; + + /** + * The language of your code. + * Supports all languages listed here: https://github.com/shikijs/shiki/blob/main/docs/languages.md#all-languages + * + * @default "plaintext" + */ + lang?: string; + + /** + * Optional tags marking beginning and end of content to include + * Loosely based on https://docs.asciidoctor.org/asciidoc/latest/directives/include-tagged-regions/ + * Note: Currently only supports one set of tags. + */ + tags?: string; +} + +const { url, lang = 'plaintext', tags} = Astro.props as Props; +import { Code } from 'astro/components'; +const remoteResponse = await fetch(url); +const remoteCode = await remoteResponse.text(); +let remoteSelectedCode; + +if (tags) { + remoteSelectedCode = selectTagged(remoteCode, tags); +} else { + const matchLines = url.match(/.*#L(\d+)(-L(\d+))?$/); + if (matchLines) { + remoteSelectedCode = selectLines(remoteCode, matchLines[1], matchLines[3]); + } else { + remoteSelectedCode = remoteCode.trim(); + } +} + +function selectTagged(content: string, tags: string): string { + const lines = remoteCode.split("\n"); + + const startLine = lines.findIndex((line) => line.includes(`tag::${tags}`)); + const endLine = lines.findIndex((line) => line.includes(`end::${tags}`)); + return lines.slice(startLine + 1, endLine).join("\n"); +} + +function selectLines(content: string, lineNum: string, lineEnd: string): string { + const lines = content.split("\n"); + if (lineNum && lineEnd) { + return lines.slice(lineNum-1, lineEnd-1).join("\n"); + } else if (lineNum) { + return lines[lineNum - 1]; + } else { + return content; + } +} +--- + diff --git a/astro/src/components/quickstarts/Index.astro b/astro/src/components/quickstarts/Index.astro new file mode 100644 index 0000000000..7980b6df5e --- /dev/null +++ b/astro/src/components/quickstarts/Index.astro @@ -0,0 +1,13 @@ +--- +import ClickableCard from "../ClickableCard.astro"; +import { getCollection } from "astro:content"; + +const { section } = Astro.props; +const tools = await getCollection("quickstarts"); +--- + +
+ {tools.map(tool => + + )} +
\ No newline at end of file diff --git a/astro/src/content/config.js b/astro/src/content/config.js index f7e7fb1657..d3a67de132 100644 --- a/astro/src/content/config.js +++ b/astro/src/content/config.js @@ -26,7 +26,22 @@ const devToolsCollection = defineCollection({ }), }); +const quickstartsCollection = defineCollection({ + schema: z.object({ + author: z.string().optional(), + description: z.string(), + disableTOC: z.boolean().default(false), + excludeFromNav: z.boolean().default(false), + icon: z.string().optional(), + section: z.string(), + title: z.string(), + sortTitle: z.string().optional(), + featured: z.boolean().default(false), + }), +}); + export const collections = { 'articles': articlesCollection, 'dev-tools': devToolsCollection, + 'quickstarts': quickstartsCollection, }; diff --git a/astro/src/content/quickstarts/index.mdx b/astro/src/content/quickstarts/index.mdx new file mode 100644 index 0000000000..b757ecc902 --- /dev/null +++ b/astro/src/content/quickstarts/index.mdx @@ -0,0 +1,9 @@ +--- +title: Quickstarts +description: Enjoy these web based developer tools that our team has put together. +disableTOC: true +section: web +--- +import Index from "../../components/quickstarts/Index.astro"; + + diff --git a/astro/src/content/quickstarts/quickstart-ruby-rails-web.mdx b/astro/src/content/quickstarts/quickstart-ruby-rails-web.mdx new file mode 100644 index 0000000000..ba7faf1429 --- /dev/null +++ b/astro/src/content/quickstarts/quickstart-ruby-rails-web.mdx @@ -0,0 +1,195 @@ +--- +title: Quickstart Ruby on Rails and FusionAuth +description: Quickstart integration of Ruby on Rails web application with FusionAuth +navcategory: getting-started +prerequisites: Ruby, bundler and Rails +section: web +technology: Ruby on Rails +language: Ruby +--- +import RemoteCode from '/src/components/RemoteCode.astro'; + +In this tutorial, you are going to learn how to integrate a Ruby on Rails application with FusionAuth. +The docker compose file and source code for a complete application are available at +https://github.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web + +## Prerequisites + +- [Ruby](https://www.ruby-lang.org/en/documentation/installation/): This will be needed for pulling down the various dependencies. +- [Rails](https://guides.rubyonrails.org/getting_started.html): This will be used in order to run the Rails server. +- [Docker](https://www.docker.com): The quickest way to stand up FusionAuth. + - (Alternatively, you can [Install FusionAuth Manually](https://fusionauth.io/docs/v1/tech/installation-guide/)). + +This app has been tested with Ruby 3.2.2 and Rails 7.0.4.3 + +## FusionAuth Installation via Docker + +The root of this project directory (next to this README) are two files [a Docker compose file](./docker-compose.yml) and an [environment variables configuration file](./.env). Assuming you have Docker installed on your machine, you can stand up FusionAuth up on your machine with: + +``` +git clone https://github.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web.git +cd fusionauth-quickstart-ruby-on-rails-web +docker-compose up -d +``` + +The FusionAuth configuration files also make use of a unique feature of FusionAuth, called [Kickstart](https://fusionauth.io/docs/v1/tech/installation-guide/kickstart): when FusionAuth comes up for the first time, it will look at the [Kickstart file](./kickstart/kickstart.json) and mimic API calls to configure FusionAuth for use when it is first run. + +> **NOTE**: If you ever want to reset the FusionAuth system, delete the volumes created by docker-compose by executing `docker-compose down -v`. + +FusionAuth will be initially configured with these settings: + +* Your client Id is: `e9fdb985-9173-4e01-9d73-ac2d60d1dc8e` +* Your client secret is: `super-secret-secret-that-should-be-regenerated-for-production` +* Your example username is `richard@example.com` and your password is `password`. +* Your admin username is `admin@example.com` and your password is `password`. +* Your fusionAuthBaseUrl is 'http://localhost:9011/' + +You can log into the [FusionAuth admin UI](http://localhost:9011/admin) and look around if you want, but with Docker/Kickstart you don't need to. + +## Create your Ruby on Rails Application + +Now you are going to create a Ruby on Rails application. While this section builds a +simple Ruby on Rails application, you can use the same configuration to +integrate your existing Ruby on Rails application with FusionAuth. + +```bash +rails new myapp && cd myapp +``` + +### Configure Omniauth + +Install the omniauth gem and other supporting gems. Add the following three lines to your `Gemfile`. + + + +Then, install them. + +```bash +bundle install +``` + +Next, update your `config/environments/development.rb` file with FusionAuth OpenID Connect (OIDC) environment specific configuration information. + +> **NOTE**: You'll have to add similar configuration to the relevant environment files when deploying to prod or other environments. + + + +Configure Omniauth by creating `config/initializers/omniauth.rb` with the following + + + +This pulls from the environment file and configures omniauth to communicate with FusionAuth. + +### Add Controllers + +Next, you can create some controllers with the following shell commands: + +```shell +rails generate controller auth +rails generate controller home +rails generate controller make_change +``` + +These controllers have the following purposes: + +* `auth` is for omniauth integration +* `home` is an unprotected home page with a login button +* `make_change` is a protected page for our example bank application + +First, let's update the `config/routes.rb` file. Here's what that should look like: + + + +Some simple routes corresponding to the controllers: + +* `make_change` is the protected bank page +* `home` is the home page, which is available to unauthenticated users. This is also the default page. +* `logout` is tied to the auth controller's logout method. +* `auth/:provider/callback` is the omniauth callback method, which completes the OpenID Connect (OIDC) grant. + +Now, update the auth controller at `app/controllers/auth_controller.rb` to look like this, which fulfills some of the routes above. + + + +This lets us have a nice `logout` method and also handle the callback from omniauth. The latter sets a `session` attribute with user data, which can be used by views later. + +Now, update the application controller at `app/controllers/application_controller.rb`. + + + +- `authenticate_user!` enforces authentication for all routes in your application by checking for the session attribute set by the auth controller after a successful login. +- `redirect_non_localhost!` ensures users access the web app via `localhost` instead of a url like http:127.0.0.1:3000/ . FusionAuth's origin and redirect URL configurations in this example expect `localhost`. In production, similar logic should be updated with your domain name. + +Now, let's build out the home page. Update the home controller at `app/controllers/home_controller.rb` to look like this: + + + +You're skipping authentication for this route. This is so that a user has someplace to go if they are unauthenticated. + +### Add Views + +The view is also welcoming, but prompts them to login. Otherwise, it shows a mock account balance. Replace `app/views/home/index.html.erb` with this: + + + +Finally, update the layout so the user has login or logout buttons on every page. Add the below code to `app/views/layouts/application.html.erb` just after the `` tag. + + + +### Add Styling + +Now, add some image assets and styling to make this look like a real application with the following shell commands: + +```shell +wget -o app/assets/images/example_bank_logo.svg https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/complete-app/app/assets/images/example_bank_logo.svg +wget -o app/assets/images/fusion_auth_logo.svg https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/complete-app/app/assets/images/fusion_auth_logo.svg +wget -o app/assets/images/money.jpg https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/complete-app/app/assets/images/money.jpg +wget -o app/assets/stylesheets/changebank.css https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/complete-app/app/assets/stylesheets/changebank.css +``` + +Once you’ve created these files, you can test the application. + +## Run the App + +Start up the Rails application using this command: + +```shell +OP_SECRET_KEY="super-secret-secret-that-should-be-regenerated-for-production" bundle exec rails s +``` + +`OP_SECRET_KEY` is the client secret, which was defined by the [FusionAuth Installation via Docker](#fusionauth-installation-via-docker) step. You don't want to commit secrets like this to version control, so use an environment variable. + +You can now open up an incognito window and visit the Rails app at http://localhost:3000 . +Log in with the user account you created when setting up FusionAuth, and you’ll see the email of the user next to a logout button. + +## Troubleshooting + +* I get `This site can’t be reached localhost refused to connect.` when I click the Login button + +Ensure FusionAuth is running in the Docker container. You should be able to login as the admin user, `admin@example.com` with the password of `password` at http://localhost:9011/admin + +* I get an error page when I click on the Login button with message of `"error_reason" : "invalid_client_id"` + +Ensure the value for `config.x.fusionauth.client_id` in the file `config/environments/development.rb` matches client id configured in FusionAuth for the Example App application at http://localhost:9011/admin/application/ + +* I'm getting an error from Rails after logging in + +``` +Rack::OAuth2::Client::Error +invalid_client :: Invalid client authentication credentials. +``` + +This indicates that Omniauth is unable to call FusionAuth to validate the returned token. It is likely caused my not supplying the correct *client secret*. Ensure the `OP_SECRET_KEY` used to start rails matches the FusionAuth ExampleApp client secret. http://localhost:9011/admin/application/ \ No newline at end of file diff --git a/astro/src/layouts/Quickstart.astro b/astro/src/layouts/Quickstart.astro new file mode 100644 index 0000000000..040dbff037 --- /dev/null +++ b/astro/src/layouts/Quickstart.astro @@ -0,0 +1,24 @@ +--- +import { getCollection } from "astro:content"; +import Default from "./Default.astro"; + +const quickstarts = await getCollection("quickstarts"); +const sections = []; +for (const quickstart of quickstarts) { + if (quickstart.id.indexOf("index.md") !== -1) { + continue; + } + + sections.push( + { + "title": quickstart.data.title, + "path": `/quickstarts/${quickstart.slug}`, + "icon": quickstart.data.icon, + "color": quickstart.data.color + } + ) +} +--- + + + diff --git a/astro/src/pages/quickstarts/[...slug].astro b/astro/src/pages/quickstarts/[...slug].astro new file mode 100644 index 0000000000..ecb19b65e4 --- /dev/null +++ b/astro/src/pages/quickstarts/[...slug].astro @@ -0,0 +1,17 @@ +--- +import { getCollection } from "astro:content"; +import Layout from "../../layouts/Quickstart.astro"; + +export async function getStaticPaths() { + const entry = await getCollection("quickstarts"); + return entry.map(entry => ({ + params: { slug: entry.slug }, props: { entry }, + })); +} + +const { entry } = Astro.props; +const { Content, headings } = await entry.render(); +--- + + + diff --git a/astro/src/pages/quickstarts/index.astro b/astro/src/pages/quickstarts/index.astro new file mode 100644 index 0000000000..5fb3b647cd --- /dev/null +++ b/astro/src/pages/quickstarts/index.astro @@ -0,0 +1,19 @@ +--- +import ClickableCard from "../../components/ClickableCard.astro"; +import Layout from "../../layouts/Quickstart.astro"; + +import { getCollection } from "astro:content"; +const quickstarts = await getCollection("quickstarts"); +--- + + +

+ Try these quickstarts to quickly get started using FusionAuth with these frameworks. +

+
+ {quickstarts.filter(quickstart => quickstart.id.indexOf("index.md") === -1) + .map(quickstart => + + )} +
+