{"id":13558250,"url":"https://github.com/openstax/accounts","last_synced_at":"2026-04-11T21:14:51.979Z","repository":{"id":9427551,"uuid":"11300066","full_name":"openstax/accounts","owner":"openstax","description":"OpenStax centralized authentication and accounts service","archived":false,"fork":false,"pushed_at":"2026-02-17T05:41:18.000Z","size":9113,"stargazers_count":18,"open_issues_count":16,"forks_count":7,"subscribers_count":20,"default_branch":"main","last_synced_at":"2026-02-17T11:33:19.742Z","etag":null,"topics":["authentication","hacktoberfest","rails","ruby"],"latest_commit_sha":null,"homepage":"https://accounts.openstax.org","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/openstax.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":"COPYRIGHT","agents":null,"dco":null,"cla":null}},"created_at":"2013-07-10T02:36:07.000Z","updated_at":"2026-02-16T22:41:20.000Z","dependencies_parsed_at":"2026-01-16T11:08:50.432Z","dependency_job_id":null,"html_url":"https://github.com/openstax/accounts","commit_stats":null,"previous_names":[],"tags_count":381,"template":false,"template_full_name":null,"purl":"pkg:github/openstax/accounts","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openstax%2Faccounts","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openstax%2Faccounts/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openstax%2Faccounts/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openstax%2Faccounts/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/openstax","download_url":"https://codeload.github.com/openstax/accounts/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openstax%2Faccounts/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29646654,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-20T08:48:14.886Z","status":"ssl_error","status_checked_at":"2026-02-20T08:45:26.777Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","hacktoberfest","rails","ruby"],"created_at":"2024-08-01T12:04:50.245Z","updated_at":"2026-02-20T09:10:32.560Z","avatar_url":"https://github.com/openstax.png","language":"Ruby","readme":"# OpenStax Accounts\n\n[![Tests](https://github.com/openstax/accounts/workflows/Tests/badge.svg)](https://github.com/openstax/accounts/actions?query=workflow:Tests)\n[![Migrations](https://github.com/openstax/accounts/workflows/Migrations/badge.svg)](https://github.com/openstax/accounts/actions?query=workflow:Migrations)\n[![codecov](https://codecov.io/gh/openstax/accounts/branch/main/graph/badge.svg?token=AL4prAnF2K)](https://codecov.io/gh/openstax/accounts)\n\n![](https://codebuild.us-east-1.amazonaws.com/badges?uuid=eyJlbmNyeXB0ZWREYXRhIjoiUmNNNVBKRVpIOXBHOFVyNldNTnNrc0hQaTdrSUZGSGtOcnpLV1ZwL2FnMmVwY3l3RUp4OXpFOTNycEV0a2p4N29yampiZnVjN1MyYU5SM3VBMU8vWm9nPSIsIml2UGFyYW1ldGVyU3BlYyI6IldkemJaMFQxUW51Qnl2VGEiLCJtYXRlcmlhbFNldFNlcmlhbCI6MX0%3D\u0026branch=main)\n![](https://img.shields.io/github/v/tag/openstax/accounts?label=latest%20tag)\n\nOpenStax Accounts is a centralized User Account services provider for various OpenStax products, including:\n\n* Authentication and authorization\n* Profile/personal data\n* Email notifications\n* OAuth Application privileges\n\nIt uses OAuth mechanisms and API keys to provide these services to OpenStax products and their users.\n\nAccounts requires the repeatable read isolation level to work properly. If using PostgreSQL, add the following to your `postgresql.conf`:\n\n```Ruby\ndefault_transaction_isolation = 'repeatable read'\n```\n\n## Usage — topics\n* [Different ways to create a user account](#different-ways-to-create-a-user-account)\n* [What happens during sign up](#what-happens-during-sign-up)\nWhich records need to be created and why.\n* [Logging in](#logging-in)\nHow we check a user's credentials.\n* [GDPR](#gdpr)\nFor compliance with the General Data Protection Regulation (a regulation in the European Union to protect their citizens' data and privacy).\n* [Special Parameters](#special-parameters)\nThe app behavior changes depending on the value of these parameters.\n* [Salesforce](#salesforce)\n\n## Concepts\n* [Lev handlers](#lev-handlers)\n* [Lev routines](#lev-routines)\n* [Leads](#\"leads\")\n\n## Dev Environment Setup\n\nAccounts can be run as a normal Rails app on your machine or in a Docker container.\n\n### Docker\n\n```bash\n# build the image\n$\u003e docker-compose build\n# run it; Accounts available at localhost:2999\n$\u003e docker-compose up -d\n# run it allowing for debugging\n$\u003e docker-compose up -d \u0026\u0026 docker attach accounts_app_1\n```\n\n### Directly on your machine\n\n#### Database setup\n\nIf you don't have postgresql already installed, on Mac:\n\n```sh\n$ brew install postgresql\n$ brew services start postgresql\n$ psql postgres\nCREATE ROLE ox_accounts WITH LOGIN;\nALTER USER ox_accounts WITH SUPERUSER;\n\\q\n```\n\n#### Running as a normal Rails app on your machine\n\nFirst, ensure you have a Ruby version manager installed, such as [rbenv](https://github.com/rbenv/rbenv#installation) or RVM to manage your ruby versions. Then, install the Ruby version specified in the `.ruby-version` file (3.1.6 at the time of this writing, or above).\n\nTo start running Accounts in a development environment, clone the repository and then run:\n\n```sh\n$ bundle install --without production\n```\n\nJust like with any Rails app, you need to create, migrate, and then seed the database with some default records:\n\n```sh\n$ rake db:create db:setup\n```\n\nBefore starting the server, you'll need to create a  `.env` file based off of the example:\n\n```sh\n$ cp .env.example .env\n```\n\nNow you can run:\n\n```sh\n$ rails server\n```\n\nwhich will start Accounts up on port 2999. Visit http://localhost:2999.\n\n#### Running background jobs\n\nAccounts in production runs background jobs using `delayed_job`.\nIn the development environment, however, background jobs are run \"inline\", i.e. in the foreground.\n\nTo actually run these jobs in the background in the development environment,\nset the environment variable `USE_REAL_BACKGROUND_JOBS=true` in your `.env` file\nand then start the `delayed_job` daemon:\n\n`bin/rake jobs:work`\n\n## Running Specs (Automated Tests)\n\nUsing Docker, you can\n\n```sh\n$\u003e docker-compose run --rm app /bin/bash\n```\n\nto drop into a container with everything installed. Then before your first test run\n\n```sh\n$ /code\u003e rake db:create\n$ /code\u003e rake db:migrate # run again if add migrations later\n```\n\nThen to run tests...\n\n```sh\n$ /code\u003e rspec\n$ /code\u003e rspec ./spec/features\n$ /code\u003e rspec ./spec/some/specific_spec.rb\n$ /code\u003e rspec ./spec/some/specific_spec.rb:42 # the spec at line 42\n```\n\nOr if you want to install directly on your machine...\n\nSpecs require phantomjs. On Mac:\n```sh\n$ brew install phantomjs\n```\n\nTo run specs,\n\n```sh\n$ rake\n```\n\nWhen running feature specs, the default behavior is for exceptions to be rescued and nice error pages to be shown.  This can make debugging difficult if you're not expecting an error.  To not rescue exceptions, do:\n\n```sh\n$ RAISE=true rspec\n```\n\nIf you encounter issues running features specs, check the version of chromedriver you have installed.  Version 2.38 is known to work.\n\n## Debugging\n\nSet `DEBUGGER=byebug` to use byebug (e.g. in `.env`), otherwise defaults to the VS Code debugger.\n\n# Usage — topics\n\n## Different ways to create a user account\n\n### User arrives at the signup page\nThe most straightforward way to sign up is by visiting the `newflow_signup_path`  without any parameters in the URL and without any state in the session.\n\n### User arrives at a restricted page (one which calls `authenticate_user!`) **with** _signed parameters_\n`authenticate_user!` calls `use_signed_params` which is used to either 1) automatically log in users or 2) prefill/preselect their information in the sign up forms like their `role` and `email` address.\n\nThis feature is primarily used for logging in students via an LMS (Learning Management System). The LMS sends a student's info to Tutor and Tutor signs the request and sends it to Accounts\n\n### Via OAuth — a doorkeeper application sends users to oauth_authorization_path\nAn OAuth application consumer may send users to `/oauth/authorize` url endpoint (along with the `client_id` and other params specified in the OAuth protocol definition). When this happens, assuming there's no currently logged in user to Accounts, user is redirected to the login page. Once logged in, the user is redirected back to the application consumer. Note that for trusted `oauth_applications`, we skip authorization of the application consumer (see [config/initializers/doorkeeper.rb](config/initializers/doorkeeper.rb) under skip_authorization).\n\n### Via the API\nAn OAuth (doorkeeper) application may make a request to an API endpoint (POST `/user/find-or-create`) to create users.\n\n### Imported via a rake task\n\nSee [import_users.rake](lib/tasks/accounts/import_users.rake).\n\n## What happens during sign up\nWhich records need to be created and why.\n\n* A `User` record, of course, needs to be created. But this record doesn't contain the authentication credentials. It _is_ the main model for users—stores `uuid`, `first_name`, `last_name`, `username`, etc.— but everything else is stored via associated models.\n* An `Authentication` record. This model/record stores the different ways that a user may login, for example, using Facebook, Google, or using email and password. Having a separate model/record for this makes it easier to add new ways of logging in or signing up.\n* An `Identity` record. Essentially, this model stores the **password** for any given `Authentication` and provides a way to check against a user-submitted password during login (calling the method `authenticate`). `Authentication`s (and therefore `User`s) that don't have a password set up will not have an `Identity`.\n* An `ApplicationUser` if the user is signing up as they authorize a doorkeeper/OAuth application at the same time basically. See [config/initializers/doorkeeper_models.rb](config/initializers/doorkeeper_models.rb), calls `FindOrCreateApplicationUser` on `before_create`. Also, if the user account is being created by an application via the api (`/user/find-or-create`) an `ApplicationUser` is created. `FindOrCreateUser` calls `FindOrCreateApplicationUser`. This is in order to associate a user with an application. An application may only handle its own users, unless it's one of our own, trusted, applications.\n* A `ContactInfo` record which essentially stores email addresses for users.\n\n## Logging in\nHow we check a user's credentials.\n\nUnder the hood, we use `BCrypt`'s `authenticate` method to safely compare users' provided password against the `password_digest` we've stored on sign up.\n\nOne thing we do differently from what is advised as the most secure way of authenticating users is: we let our users know whether they've just entered the wrong password for an account which in fact does exist in our database, or if there is not a (verified) account associated with the provided email or username. This is a tradeoff we make in order to be more friendly with our users but at the same time, we do have rate limiting in place so the security risk is minimal.\n\nThere is a feature that allows authenticating against a password which was created in CNX—which is a web application we own and use for editing our books' content. See [app/models/identity](app/models/identity) to see how we do this.\n\n## Cloudfront\n\nAccounts is able to run with all URLs using an `/accounts` path prefix.  This lets us put Accounts under a Cloudfront distribution and route\nall `/accounts/*` requests to it.  Test this out by adding `/accounts` to the start of any page's path -- navigating from that point forward\nshould keep you in an `/accounts` path prefix.\n\nIf for some reason Accounts ever causes you to leave the `/accounts` prefix and just return to normal routes, this will be a problem when\nAccounts is deployed to Cloudfront, because Cloudfront won't route that request to Accounts.  We added some middleware to Accounts that will\nfreak out if this happens.  You can run the rails server with a `SIMULATE_CLOUDFRONT=true` environment variable and the server will raise an exception if it ever receives a URL without the `/accounts` prefix.  This is useful for clicking around and making sure we have accounted for all of the routes.\n\nYou can also use this environment variable when running tests, but note that the expectations on paths (\"expect page to have path blah\") have\nnot been updated to expect the `/accounts` prefix.\n\n## Salesforce\n\nA salesforce user must be signed in through the admin console for the Salesforce stuff to work — Salesforce \u003e Setup \u003e Set Salesforce User\n\n## GDPR\n\nFor logged-in users, Accounts reports GDPR status in the `/api/user` endpoint via a `is_not_gdpr_location` flag.  When this value is `true`, the user is not in a GDPR location.  Otherwise (`false` or `nil` or not in the response), the user may be in a GDPR location.  To test this functionality in development, you can specify an IP address via the `IP_ADDRESS_FOR_GDPR` environment variable, which will override the normal localhost request IP address.\n\n## Special parameters\nThe app behavior changes depending on the value of these parameters.\n\n### `go` parameter\n\n* OAuth requests that arrive with query param `go=signup` will skip log in and go straight to signup.\n* OAuth requests that arrive with query param `go=student_signup` will skip to signup and cause the signup form to have the \"student\" role.\n\n### `sp` parameter\nShort for \"signed parameters\", requests that arrive with a valid `sp` parameter may force Accounts to automatically log in a user with the given ID (\"valid\" meaning signed by a `Doorkeeper::Application` configured in Accounts). Or, if no user found by the `uuid` parameter, then the signup form is pre-populated with the rest of the \"signed parameters.\"\n\n### `signup_at` parameter along with `client_id`, only on `/login` page\nWhen a request comes with both `signup_at` and `client_id` parameters in the login page, the **Sign up here** link points to `signup_at` which has to listed as a callback urls in the client (OAuth) application with ID equal to `client_id`.\n\nFor example:\nhttps://dev.accounts.openstax.org/login?signup_at=https://tutor-dev.openstax.org/signup\u0026client_id=1234\n\n### `r` parameter\nShort for `r`edirect parameter, if present and trusted, we store it in order to redirect users back to the OpenStax app they came from – at the end of the login/signup process. See [save_redirect](https://github.com/openstax/accounts/blob/e48dd5d4a4bdb7bf4b1e6caa808243432ecd4f57/config/initializers/controllers.rb#L52-L60) which happens as a `before_action` in all controllers.\n\nAlso, note that OSWeb/the CMS may use the `next` parameter instead of `r` [(link)](https://github.com/openstax/openstax-cms/blob/81904f6b115fc280745c01316e3f478668893efa/oxauth/views.py#L14-L16) but it rewrites it as `r` for usage in Accounts.\n\n### `redirect_uri` parameter\nPart of the OAuth protocol, we also take advantage of its presence in the Referrer when users wish to exit Accounts and go back to the OAuth app they came from. See the controller action `exit_accounts` [here](https://github.com/openstax/accounts/blob/e48dd5d4a4bdb7bf4b1e6caa808243432ecd4f57/app/controllers/newflow/login_signup_controller.rb#L338-L339).\n\n# Concepts\nThese are things that are specific to Accounts and/or will help you learn and understand the codebase better.\n\n## Lev (gem) — \"Ride the rails but don't touch them.\"\n[Lev](https://github.com/lml/lev), short for Levitate, is a gem developed by us to facilitate writing clean, modular, testable business logic encapsulated in a database transaction.\n\n## Lev handlers\nPractically, they process user-submitted forms. For example:\n\n```Ruby\nclass FindUser\n  lev_handler\n\n  paramify :login do # available as `login_params`\n      attribute :username_or_email, type: String\n  end\n\nprotected\n\n  def authorized?\n    # Check permissions for [action] by [current user]\n  end\n\n  def handle\n    # Do the work.\n    user = User.where(login_params).first\n    # Add outputs to `outputs` (a `Hashie::Mash` object).\n      outputs.user = user\n      outputs[:foo] = 'bar'\n\n    # Add errors to the `errors` object, if any.\n    errors.add(true, code: :foo)\n  end\nend\n```\n\nWe use them in all controllers with `handle_with` which takes lambdas in `success` and `failure` keys, like so:\n\n```Ruby\nhandle_with(\n  success: lambda { do_something },\n  failure: lambda { do_something_else }\n)\n```\n\nInside of the context of success and failure lambdas, is a `@handler_result` variable which contains `outputs` set inside of the handler (like `outputs.user = user`), if any, or an `errors` object, if any, which can be populated with `fatal_error` or `transfer_errors_from`.\n\nLev Handlers **must** implement two instance methods:\n\n1. `handle`, which takes no arguments and does the work the handler is charged with\n2. `authorized?`, which returns true if and only if the caller is authorized to do what the handler is charged with\n\nHandlers may...\n\n1. Implement the `setup` instance method which runs before `authorized?` and `handle`. This method can do anything, and will likely include setting up some instance objects based on the params.\n2. call the class method `paramify` to declare, cast, and validate parts of the params hash.\n\nSee https://github.com/lml/lev#handlers for more info.\n\n\n## Lev routines\nLev's Routines are pieces of code that have all the responsibility for making one thing (one use case) happen, (e.g. \"add an email to a user\", \"register a student to a class\", etc). Lev Handlers and Routines are very similar because all Handlers are Routines.\n\n```Ruby\nclass MyRoutine\n  lev_routine\n\nprotected\n\n  def exec(foo, options={})\n    fatal_error(code: :some_code_symbol) if foo.nil?\n    outputs[:foo] = foo * 2\n    outputs[:bar] = foo * 3\n  end\nend\n```\n\nSee https://github.com/lml/lev#routines for more info.\n\n## \"Leads\"\nWhen someone signs up, especially if they are Educators, we want to keep track of whether or not they have already adopted (started using) OpenStax textbooks or software. Of course, we want them to do so, and so we use Salesforce mainly to schedule marketing emails (aka. the newsletter) as well as to store their information in order to verify them as actual Educators – members of a school or homeschool teachers, librarians, etc.\n\nKeeping the above in mind will help you make sense of the signup form and related business logic such as [PushSalesforceLead](app/routines/push_salesforce_lead.rb).\n\nIn short, new users become sales leads in Salesforce.\n\n## Additional Documentation\nAdditional documentation can be found in the [Accounts Wiki](https://github.com/openstax/accounts/wiki)\n","funding_links":[],"categories":["Ruby","ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenstax%2Faccounts","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopenstax%2Faccounts","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenstax%2Faccounts/lists"}