{"id":21939193,"url":"https://github.com/grottopress/samba","last_synced_at":"2025-04-22T14:49:13.963Z","repository":{"id":85788222,"uuid":"578784794","full_name":"GrottoPress/samba","owner":"GrottoPress","description":"Single Sign On authentication for Lucky framework","archived":false,"fork":false,"pushed_at":"2025-01-22T23:42:37.000Z","size":184,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-29T16:11:08.519Z","etag":null,"topics":["authentication","crystal","lucky-framework","oauth2","security","sso"],"latest_commit_sha":null,"homepage":"","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/GrottoPress.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2022-12-15T21:55:57.000Z","updated_at":"2025-01-30T02:13:51.000Z","dependencies_parsed_at":"2023-03-04T14:00:25.822Z","dependency_job_id":"b5e71a1c-0820-4343-94f3-a30e74c47e47","html_url":"https://github.com/GrottoPress/samba","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GrottoPress%2Fsamba","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GrottoPress%2Fsamba/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GrottoPress%2Fsamba/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GrottoPress%2Fsamba/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/GrottoPress","download_url":"https://codeload.github.com/GrottoPress/samba/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250263095,"owners_count":21401799,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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","crystal","lucky-framework","oauth2","security","sso"],"created_at":"2024-11-29T02:17:19.107Z","updated_at":"2025-04-22T14:49:13.956Z","avatar_url":"https://github.com/GrottoPress.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Samba\n\n**Samba** is a Single Sign On authentication solution for [Lucky](https://luckyframework.org) framework. It extends [Shield](https://github.com/grottopress/shield)'s OAuth 2 implementation with authentication capabilities.\n\n*Samba* allows a user to log in once in an organization, and gain automatic access other apps in the organization. Conversely, when a user logs out of one app, they are automatically logged out of all other apps.\n\n*Samba* defines two roles:\n\n1. **Server**: An OAuth 2 authorization server maintained by your organization.\n\n1. **Client**: Any application within your organization, other than the *Samba* Server, whose user identification and authentication functions are handled by the Server.\n\n## Installation\n\n### The Server\n\n*You should already have an OAuth 2 authorization server. See *Shield*'s [documentation](https://github.com/GrottoPress/shield/tree/master/docs) for details.*\n\n1. Add the dependency to your `shard.yml`:\n\n   ```yaml\n   # -\u003e\u003e\u003e shard.yml\n\n   # ...\n   dependencies:\n     samba:\n       github: GrottoPress/samba\n   # ...\n   ```\n\n1. Run `shards install`\n\n1. Require *Samba* in your app:\n\n   ```crystal\n   # -\u003e\u003e\u003e src/app.cr\n\n   # ...\n   require \"samba/server\"\n   # ...\n   ```\n\n1. Require *presets*, right after models:\n\n   ```crystal\n   # -\u003e\u003e\u003e src/app.cr\n\n   # ...\n   require \"./models/base_model\"\n   require \"./models/**\"\n\n   require \"samba/presets/server\"\n   # ...\n   ```\n\n### The Client\n\n1. Add the dependency to your `shard.yml`:\n\n   ```yaml\n   # -\u003e\u003e\u003e shard.yml\n\n   # ...\n   dependencies:\n     samba:\n       github: GrottoPress/samba\n   # ...\n   ```\n\n1. Run `shards install`\n\n1. Require *Samba* in your app:\n\n   ```crystal\n   # -\u003e\u003e\u003e src/app.cr\n\n   # ...\n   require \"samba/client\"\n   # ...\n   ```\n\n1. Require *presets*, right after models:\n\n   ```crystal\n   # -\u003e\u003e\u003e src/app.cr\n\n   # ...\n   require \"./models/base_model\"\n   require \"./models/**\"\n\n   require \"samba/presets/client\"\n   # ...\n   ```\n\n## Usage\n\n### The Server\n\n1. Set up actions:\n\n   ```crystal\n   # -\u003e\u003e\u003e src/actions/oauth/authorization/create.cr\n\n   class Oauth::Authorization::Create \u003c BrowserAction\n     # ...\n     include Samba::Oauth::Authorization::Create\n\n     post \"/oauth/authorization\" do\n       run_operation\n     end\n\n     #def do_run_operation_succeeded(operation, oauth_grant)\n     #  code = OauthGrantCredentials.new(operation, oauth_grant)\n     #  redirect to: oauth_redirect_uri(code: code.to_s, state: state).to_s\n     #end\n\n     #def do_run_operation_failed(operation)\n     #  error = operation.granted.value ? \"invalid_request\" : \"access_denied\"\n     #  redirect to: oauth_redirect_uri(error: error, state: state).to_s\n     #end\n     # ...\n   end\n   ```\n\n   `Samba::Oauth::Authorization::Create` modifies `Shield::Oauth::Authorization::Create` to set the OAuth client ID in session after a successful authorization code request.\n\n   These client IDs are used to determine which `BearerLogin` tokens to revoke whenever a user logs out.\n\n   ---\n   ```crystal\n   # -\u003e\u003e\u003e src/actions/current_login/destroy.cr\n\n   class CurrentLogin::Destroy \u003c BrowserAction\n     # ...\n     include Samba::CurrentLogin::Destroy\n\n     get \"/logout\" do\n       run_operation\n     end\n\n     #def do_run_operation_succeeded(operation, login)\n     #  flash.success = Rex.t(:\"action.current_login.destroy.success\")\n     #  redirect to: New\n     #end\n\n     #def do_run_operation_failed(operation)\n     #  flash.failure = Rex.t(:\"action.current_login.destroy.failure\")\n     #  redirect_back fallback: CurrentUser::Show\n     #end\n     # ...\n   end\n   ```\n\n   This action is used for Single Sign Out. All a *Samba* Client has to do is point its logout link to this URL.\n\n1. Set up i18n:\n\n   *Samba* uses *Rex* for i18n. See \u003chttps://github.com/GrottoPress/rex\u003e.\n\n   Use the following as a guide to set up translations:\n\n   ```yaml\n   action:\n     current_login:\n       destroy:\n         failure: Something went wrong\n         success: You have logged out successfully\n\n### The Client\n\nEach *Samba* Client must be registered with the *Samba* Server as a *confidential* OAuth client, if it is a full stack monolith.\n\nIf a *Samba* Client is an API backend, each of its frontend apps, rather, must be registered with the Server. They may or may not be *confidential* OAuth clients.\n\n1. Configure:\n\n   ```crystal\n   # -\u003e\u003e\u003e config/samba.cr\n\n   Samba.configure do |settings|\n     # ...\n     # Set to `nil` if this app is an API backend. The frontend app should be\n     # doing the authorization code request.\n     settings.oauth_authorization_endpoint = \"https://samba.server/oauth/authorize\"\n\n     # The OAuth client details\n     # If this app is an API backend, set to `nil`.\n     settings.oauth_client = {\n       id: \"x9y8z7\",\n       # This URI must match what was used to register the OAuth client\n       redirect_uri: Oauth::Callback.url_without_query_params,\n       secret: \"a1b2c3\"\n     }\n\n     # Additional trusted OAuth clients whose tokens are accepted for\n     # authentication. If this app is an API backend, set this to all the OAuth\n     # client IDs of all its frontend apps. Otherwise, leave empty.\n     settings.oauth_client_ids = [\"def456\"]\n   \n     settings.oauth_token_endpoint = \"https://samba.server/oauth/token\"\n   \n     settings.oauth_token_introspection_endpoint =\n       \"https://samba.server/oauth/token/verify\"\n\n     # The challenge method to use for authorization code requests\n     settings.oauth_code_challenge_method = \"S256\"\n\n     # By default, *Samba* makes an API call to the OAuth introspection endpoint\n     # whenever a request is received. This setting allows to tweak this\n     # behaviour.\n     #\n     # For instance, you may short-circuit the call to return a locally-saved\n     # token, or a cached response from a previous call.\n     settings.verify_oauth_token = -\u003e(key : String, verify : -\u003e OauthToken) do\n       # This example uses Dude (https://github.com/GrottoPress/dude) to cache\n       # the response.\n       #\n       # `verify.call` is what actually does the API call\n       Dude.get(OauthToken, key, 1.hour) { verify.call }\n     end\n\n     # This token may be used when making token introspection requests.\n     # It is required if this app is an API backend. Otherwise, if you do\n     # not need to use it in any way, set to `nil`.\n     #\n     # This is typically a user-generated bearer token with access to the\n     # token instrospection endpoint, at least.\n     settings.server_api_token = \"g4h5i6\"\n     # ...\n\n     # A Client sends an authorization code request with the \"sso\" scope to\n     # signal to the Server this is an authentication request.\n     #\n     # Specify additional scopes to request when sending the authorization code\n     # request.\n     #\n     # (You'd typically want access to some sort of a user info endpoint\n     # that the Server exposes)\n     settings.login_token_scopes = [\"server.current_user.show\"]\n   end\n   ```\n\n1. Set up models:\n\n   ```crystal\n   # -\u003e\u003e\u003e src/models/user.cr\n\n   class User \u003c BaseModel\n     # ...\n     table :users do\n       # ...\n       column remote_id : Int64 # or `Int64?`\n       # ...\n     end\n     # ...\n   end\n   ```\n\n   The `remote_id` column is required. The type of this column should match the primary key type of the `User` model of the *Samba* Server.\n\n1. Set up migrations:\n\n   ```crystal\n   # -\u003e\u003e\u003e db/migrations/XXXXXXXXXXXXXX_create_users.cr\n\n   class CreateUsers::VXXXXXXXXXXXXXX \u003c Avram::Migrator::Migration::V1\n     def migrate\n       create :users do\n         # ...\n         add remote_id : Int64, unique: true\n         # ...\n       end\n     end\n\n     def rollback\n       drop :users\n     end\n   end\n   ```\n\n1. Set up actions:\n\n   While the setup instructions here are for full stack monoliths, there are API equivalents of each action that should be used when building a decoupled API backend.\n\n   ```crystal\n   # -\u003e\u003e\u003e src/actions/browser_action.cr\n\n   abstract class BrowserAction \u003c Lucky::Action\n     # ...\n     include Samba::LoginHelpers\n     include Samba::LoginPipes\n\n     #skip :pin_login_to_ip_address\n\n     #def do_require_logged_out_failed\n     #  flash.info = Rex.t(:\"action.pipe.not_logged_out\")\n     #  redirect_back fallback: CurrentUser::Show\n     #end\n \n     #def do_check_authorization_failed\n     #  flash.failure = Rex.t(:\"action.pipe.authorization_failed\")\n     #  redirect_back fallback: CurrentUser::Show\n     #end\n     # ...\n   end\n   ```\n\n   ---\n   ```crystal\n   # -\u003e\u003e\u003e src/actions/oauth/callback.cr\n\n   class Oauth::Callback \u003c BrowserAction\n     # ...\n     include Samba::Oauth::Token::Create\n\n     get \"/oauth/callback\" do\n       run_operation\n     end\n\n     #def do_run_operation_succeeded(operation, oauth_token)\n     #  return invalid_scope_response unless oauth_token.sso?\n     #  redirect_back fallback: CurrentUser::Show\n     #end\n\n     #def do_run_operation_failed(operation)\n     #  json({\n     #    error: \"invalid_request\",\n     #    error_description: operation.errors.first_value.first\n     #  })\n     #end\n     # ...\n   end\n   ```\n\n   This action must match the redirect URI registered for the client.\n\n1. Set up i18n:\n\n   *Samba* uses *Rex* for i18n. See \u003chttps://github.com/GrottoPress/rex\u003e.\n\n   Use the following as a guide to set up translations:\n\n   ```yaml\n   action:\n     pipe:\n       authorization_failed: You are not allowed to perform this action\n       not_logged_in: You are not logged in\n       not_logged_out: You are logged in\n\n       oauth:\n         client_not_authorized: Client is not allowed to perform this action\n         code_required: Authorization code is required\n         sso_only: Only authentication (SSO) is supported\n         state_invalid: Forged response detected!\n\n   operation:\n     error:\n       remote_id_required: Remote ID is required\n       remote_id_exists: Remote user has already been added\n\n       oauth:\n         code_required: Authorization code is required\n         client_id_required: Client ID is required\n         client_secret_required: Client secret is required\n         redirect_uri_required: Redirect URI is required\n   ```\n\n### Federation\n\nWhile *Samba* is designed for use in your own organization, it should not stand in your way if you decide to bolt on authentication from Identity Providers outside your organization.\n\nFor instance, you may add a \"Log in with GitHub\" button to your *Samba* Server's login page, that allows your users to log in with GitHub. *Samba* does not care how the user logs in.\n\nWhen your user is logged in at your *Samba* Server, however they were logged in, *Samba* would log them in automatically if the user tries to access any of your organization's apps.\n\nNote, however, that *Samba* itself cannot be used to implement a \"Log in with GitHub\" login flow, for instance. You may need to read the GitHub API, and use whatever libraries and tools they provide for such a purpose.\n\nIf you decide to go federated, only your *Samba* Server should interact with services outside your organization. The server may be registered with the third-party provider as an OAuth client for such a purpose.\n\n## Testing\n\n### The Server\n\nSee *Shield*'s [documentation](https://github.com/GrottoPress/shield/tree/master/docs) for details.\n\n### The Client\n\n#### Setting up:\n\n1. Install [`manastech/webmock.cr`](https://github.com/manastech/webmock.cr) as a development dependency\n\n1. Require *Samba* Client spec:\n\n   ```crystal\n   # -\u003e\u003e\u003e spec/spec_helper.cr\n\n   # ...\n   require \"samba/spec/client\"\n   # ...\n   ```\n\n   This pulls in various types and helpers for specs.\n\n1. Set up API client:\n\n   ```crystal\n   # -\u003e\u003e\u003e spec/support/api_client.cr\n\n   class ApiClient \u003c Lucky::BaseHTTPClient\n     def initialize\n       super\n       headers(\"Content-Type\": \"application/json\")\n     end\n   end\n   ```\n\n   *Samba* comes with `Samba::HttpClient`, which enables API and browser authentication in Client specs.\n\n#### Authenticating:\n\n- Browser authentication\n\n  ```crystal\n  client = ApiClient.new\n\n  # Creates a user and logs them in with a fake token.\n  # You may optionally pass in `scopes` and `session`.\n  client.browser_auth(remote_id)\n\n  # Logs in a user that is already created.\n  # You may optionally pass in `scopes` and `session`.\n  client.browser_auth(user)\n\n  # Go ahead and make requests to routes with the authenticated client.\n  client.exec(CurrentUser::Show)\n  ```\n\n- API authentication\n\n  ```crystal\n  client = ApiClient.new\n\n  # Creates a user and logs them in with a fake token.\n  # You may optionally pass in `scopes` and `session`.\n  client.api_auth(remote_id)\n\n  # Logs in a user that is already created.\n  # You may optionally pass in `scopes` and `session`.\n  client.api_auth(user)\n\n  # Go ahead and make requests to routes with\n  # the authenticated client.\n  client.exec(Api::CurrentUser::Show)\n  ```\n\n- Set cookie header from session\n\n  ```crystal\n  client = ApiClient.new\n  session = Lucky::Session.new\n\n  session.set(:one, \"one\")\n  session.set(:two, \"two\")\n\n  # Sets \"Cookie\" header from session\n  client.set_cookie_from_session(session)\n\n  # Go ahead and make requests.\n  client.exec(Numbers::Show)\n  ```\n\n## Development\n\nCreate a `.env` file:\n\n```env\nCLIENT_CACHE_REDIS_URL=redis://localhost:6379/0\nCLIENT_DATABASE_URL=postgres://postgres:password@localhost:5432/samba_client_spec\nSERVER_DATABASE_URL=postgres://postgres:password@localhost:5432/samba_server_spec\n```\n\nUpdate the file with your own details, then run tests as follows:\n\n- Run Client tests with `crystal spec spec/client`\n- Run Server tests with `crystal spec spec/server`\n\n*Do not run client and server tests together; you would get a compile error.*\n\n## Contributing\n\n1. [Fork it](https://github.com/GrottoPress/samba/fork)\n1. Switch to the `master` branch: `git checkout master`\n1. Create your feature branch: `git checkout -b my-new-feature`\n1. Make your changes, updating changelog and documentation as appropriate.\n1. Commit your changes: `git commit`\n1. Push to the branch: `git push origin my-new-feature`\n1. Submit a new *Pull Request* against the `GrottoPress:master` branch.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrottopress%2Fsamba","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgrottopress%2Fsamba","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrottopress%2Fsamba/lists"}