| 1 | --- |
| 2 | title: Single sign-on |
| 3 | description: Connect a workspace to your identity provider over SAML 2.0, OpenID Connect or OAuth 2.0, let its people join, and require it of members. On every paid plan. |
| 4 | --- |
| 5 | |
| 6 | A workspace can connect to its own identity provider, such as Okta, |
| 7 | Microsoft Entra ID, Google Workspace, Auth0, OneLogin or JumpCloud, over |
| 8 | **OpenID Connect**, **SAML 2.0** or plain **OAuth 2.0**. It comes with |
| 9 | every paid plan at no extra price. A free workspace sees the page with |
| 10 | everything turned off until an owner starts the plan. |
| 11 | |
| 12 | Single sign-on never signs anyone in to g1t. People sign in to g1t as |
| 13 | themselves, with their password, GitHub and two-factor authentication as |
| 14 | usual, then sign in through your provider to use the workspace. The |
| 15 | first time they do, g1t links who they are at your provider to their g1t |
| 16 | account. Their personal repositories and other workspaces are never your |
| 17 | provider's to open. |
| 18 | |
| 19 | ## Set it up |
| 20 | |
| 21 | Owners only, at **Workspace → Access → Single sign-on**, |
| 22 | `g1t.sh/<workspace>/-/sso`. |
| 23 | |
| 24 | 1. Choose the protocol. Use **OpenID Connect** when your provider offers |
| 25 | it; **SAML 2.0** for providers set up for SAML apps; **OAuth 2.0** for |
| 26 | one with neither. |
| 27 | 2. In your provider, create an app with the addresses the page shows: |
| 28 | |
| 29 | | | | |
| 30 | | --- | --- | |
| 31 | | Redirect URI (OpenID Connect, OAuth 2.0) | `https://g1t.sh/auth/sso/<workspace-id>/callback` | |
| 32 | | Entity id, audience (SAML) | `https://g1t.sh/auth/sso/<workspace-id>/metadata` | |
| 33 | | Assertion consumer service URL, HTTP-POST (SAML) | `https://g1t.sh/auth/sso/<workspace-id>/saml` | |
| 34 | | Metadata (SAML) | `https://g1t.sh/auth/sso/<workspace-id>/metadata` | |
| 35 | |
| 36 | They name the workspace by its id (`wsp_…`), which never changes, so |
| 37 | renaming the workspace leaves your provider's setup as it is. The page |
| 38 | shows them with the id filled in. |
| 39 | |
| 40 | 3. Copy what the provider gives you back into the page: |
| 41 | |
| 42 | - **OpenID Connect**: the issuer (where its |
| 43 | `/.well-known/openid-configuration` lives), the client id and the |
| 44 | client secret. Scopes default to `openid email profile`. |
| 45 | - **SAML 2.0**: the provider's entity id (its issuer), its single |
| 46 | sign-on URL, and its signing certificate (PEM). Paste more than one |
| 47 | while you rotate certificates. |
| 48 | - **OAuth 2.0**: the authorization, token and userinfo URLs, the client |
| 49 | id and secret, the scopes, and which claims of the userinfo answer |
| 50 | are the person's stable id (`sub`, then `id`, by default) and their |
| 51 | address (`email`). |
| 52 | |
| 53 | 4. Choose **Set up single sign-on**. For OpenID Connect g1t reads the |
| 54 | issuer's discovery document first, and says so if it cannot. |
| 55 | |
| 56 | The client secret is kept encrypted and never shown again; leave it empty |
| 57 | when you save other changes to keep it. |
| 58 | |
| 59 | ## Test it |
| 60 | |
| 61 | Choose **Test sign-in**. g1t sends you to your provider and back, links |
| 62 | your account, and marks the connection **Tested** with your name. You |
| 63 | must have signed in through it yourself before you can require it of |
| 64 | everyone. |
| 65 | |
| 66 | ## Let people join |
| 67 | |
| 68 | Turn on **Let people from your provider join** (it starts off) and anyone |
| 69 | your provider signs in becomes a member of the workspace when they first |
| 70 | sign in through it, with no invitation: send them to |
| 71 | `g1t.sh/auth/sso/<workspace>`. g1t asks them to confirm first, so a link |
| 72 | alone never makes anyone a member. They need a g1t account with a |
| 73 | confirmed email address, and the workspace's other rules still apply, such |
| 74 | as [requiring two-factor authentication](/guides/workspaces/#require-two-factor-authentication). |
| 75 | |
| 76 | **Only addresses at** limits who may join to people whose address your |
| 77 | provider vouches for (OpenID Connect's `email_verified`, or a signed SAML |
| 78 | assertion) at your domains, such as `acme.com`. Set it, or leave joining |
| 79 | off, whenever your provider lets anyone sign in: a personal Google or |
| 80 | GitHub account would otherwise let in anyone. With joining off, only |
| 81 | people who are already members can sign in through it. |
| 82 | |
| 83 | ## Require it |
| 84 | |
| 85 | Turn on **Require single sign-on** under **Test and require**. From then |
| 86 | on a member uses the workspace only: |
| 87 | |
| 88 | - **On the site**, from a session that signed in through your provider in |
| 89 | the last day. After a day they go through it again, which is usually |
| 90 | instant: your provider remembers them. |
| 91 | - **With an access token or SSH key**, once they have linked their account |
| 92 | (and, with [SCIM](#scim), while your provider still has them assigned). |
| 93 | - **Never with their g1t password** for git over HTTPS: use a token. |
| 94 | |
| 95 | A member who has not keeps their place but cannot use the workspace: its |
| 96 | private repositories, pages and API answer as if they were not a member, |
| 97 | including repositories they have a role on directly, and every page shows |
| 98 | a notice with a link to sign in through your provider. |
| 99 | |
| 100 | **Owners are never held out**, so a provider that breaks or a certificate |
| 101 | that expires never locks you out of your own workspace. Outside |
| 102 | collaborators are not asked: they are not your provider's people. |
| 103 | |
| 104 | ## Linked people |
| 105 | |
| 106 | The page lists everyone who has signed in through your provider, who they |
| 107 | are there, and when they last did. **Unlink** someone to let them link |
| 108 | another account at your provider; their sessions go through it again. |
| 109 | Each account at your provider links to one g1t account, and each g1t |
| 110 | account to one account at your provider. |
| 111 | |
| 112 | Changing the provider itself (the protocol, the issuer, or the SAML entity |
| 113 | id) unlinks everyone and stops requiring it, until someone has signed in |
| 114 | through the new one. Changing the client, the scopes or the certificate |
| 115 | keeps everyone linked. |
| 116 | |
| 117 | ## What g1t checks |
| 118 | |
| 119 | - **OpenID Connect and OAuth 2.0** use the authorization code flow with |
| 120 | PKCE. For OpenID Connect, the ID token must be issued by your issuer, for |
| 121 | your client id, unexpired, and answer this sign-in's nonce, and its |
| 122 | signature must verify with a key your provider publishes (its |
| 123 | `jwks_uri`): RS256, RS384, RS512, PS256, PS384, PS512 or ES256. Tokens |
| 124 | signed with a shared secret (HS256) or not at all are refused. |
| 125 | - **SAML** responses must be signed, the assertion or the whole response, |
| 126 | with RSA and SHA-256, SHA-384 or SHA-512, by one of the certificates you |
| 127 | saved. The response must answer the request g1t sent (sign-ins started |
| 128 | at your provider are refused), be addressed to the workspace's assertion |
| 129 | consumer service, name g1t's entity id as the audience, come from your |
| 130 | provider's entity id, and be in its validity window. When only the |
| 131 | assertion is signed, its own subject confirmation must name g1t's |
| 132 | request. Responses with comments or processing instructions are refused. |
| 133 | Leave the assertion unencrypted: it reaches g1t over HTTPS. |
| 134 | |
| 135 | ## If the plan ends |
| 136 | |
| 137 | The settings stay. Members cannot sign in through your provider until the |
| 138 | plan starts again, so a workspace that requires it holds its members out |
| 139 | meanwhile; owners can still turn the requirement off. Turning it off and |
| 140 | removing it never need a plan. |
| 141 | |
| 142 | ## Remove it |
| 143 | |
| 144 | **Danger zone → Remove** forgets your provider and who everyone is there, |
| 145 | and stops requiring it. Members keep their place in the workspace. |
| 146 | |
| 147 | ## Audit log |
| 148 | |
| 149 | Recorded in the workspace's [audit log](/guides/audit-log/): |
| 150 | `sso.created`, `sso.updated`, `sso.deleted`, `sso.required`, |
| 151 | `sso.not_required`, `sso.identity_linked`, `sso.identity_unlinked`, and |
| 152 | `member.joined_by_sso`. |
| 153 | |
| 154 | ## API and MCP |
| 155 | |
| 156 | Owners can do all of it with an access token, through the REST API or the |
| 157 | `workspace` tool's actions on the MCP server: |
| 158 | |
| 159 | | | REST | MCP action | Scope | |
| 160 | | --- | --- | --- | --- | |
| 161 | | Read it, with where to point the provider | `GET /workspaces/{workspace}/sso` | `get_sso` | `workspace:read` | |
| 162 | | Set it up or change it | `PUT /workspaces/{workspace}/sso` | `configure_sso` | `workspace:admin` | |
| 163 | | Require it, or stop | `PUT /workspaces/{workspace}/sso/required` | `set_sso_required` | `workspace:admin` | |
| 164 | | Remove it | `DELETE /workspaces/{workspace}/sso` | `delete_sso` | `workspace:admin` | |
| 165 | | The linked people | `GET /workspaces/{workspace}/sso/identities` | `list_sso_identities` | `access:read` | |
| 166 | | Unlink someone | `DELETE /workspaces/{workspace}/sso/identities/{username}` | `unlink_sso_identity` | `access:admin` | |
| 167 | |
| 168 | `configure_sso` takes `protocol` (`oidc`, `oauth2` or `saml`) and the |
| 169 | protocol's fields as the page names them: `issuer`, `client_id`, |
| 170 | `client_secret`, `scopes`; `authorization_url`, `token_url`, |
| 171 | `userinfo_url`, `subject_claim`, `email_claim`; `idp_entity_id`, |
| 172 | `idp_sso_url`, `certificate` (PEM). Its answer, like `get_sso`'s, has |
| 173 | `service_provider`: the `redirect_uri`, `entity_id`, `acs_url`, |
| 174 | `metadata_url` and `sign_in_url` to give your provider and your people. |
| 175 | The client secret is never returned. |
| 176 | |
| 177 | ```sh |
| 178 | curl -X PUT https://api.g1t.sh/workspaces/acme/sso -H "Authorization: Bearer $G1T_TOKEN" -d '{"protocol": "oidc", "issuer": "https://acme.okta.com", "client_id": "0oa1b2c3", "client_secret": "…"}' |
| 179 | ``` |
| 180 | |
| 181 | Signing in through the provider is a trip in the browser, so it has no |
| 182 | API: send people to `sign_in_url`. |
| 183 | |
| 184 | ## SCIM |
| 185 | |
| 186 | With SCIM, your provider tells g1t who is assigned to the workspace, and |
| 187 | who is not any more: |
| 188 | |
| 189 | - Only people your provider has assigned, and not deactivated, can sign in |
| 190 | through it. Someone else is told to ask whoever runs your provider. |
| 191 | - Someone it unassigns, deactivates or deletes leaves the workspace at |
| 192 | once: their membership, their roles on its repositories and teams, their |
| 193 | link, and their sessions through it. Their g1t account is theirs and |
| 194 | stays. **Owners are never removed this way**; the audit log says so, and |
| 195 | you remove one by hand if they should go. |
| 196 | - People, not groups. Assign a group to the app in your provider and its |
| 197 | members are provisioned one by one. Groups your provider pushes are |
| 198 | accepted and not kept, so pushing them never fails. |
| 199 | |
| 200 | To turn it on, open **Single sign-on → SCIM provisioning** and choose |
| 201 | **Turn on SCIM**. Give your provider: |
| 202 | |
| 203 | | | | |
| 204 | | --- | --- | |
| 205 | | SCIM base URL (tenant URL) | `https://api.g1t.sh/scim/v2/<workspace-id>` | |
| 206 | | Authentication | Bearer token: the SCIM token, shown once | |
| 207 | | Unique identifier | `userName` | |
| 208 | | Supported | Create, update (PUT and PATCH), deactivate and delete users; filtering by `userName`, `externalId`, `id` or `emails.value` | |
| 209 | |
| 210 | Whoever signs in through your provider is matched to the person it |
| 211 | assigned by a stable id it gave (the SAML NameID, OpenID's `sub`, and for |
| 212 | Microsoft Entra ID the object id and a member's user principal name) as |
| 213 | their `userName` or `externalId`, or by an address your provider vouches |
| 214 | for as their `userName` or primary address. If more than one assigned |
| 215 | person matches, nobody is chosen and they are told so. **Make a new |
| 216 | token** replaces the old one at once; **Turn off SCIM** forgets who was |
| 217 | assigned and removes nobody. |
| 218 | |
| 219 | Through the API: `POST /workspaces/{workspace}/sso/scim` (`enable_scim`) |
| 220 | answers with the token and `scim_url`; `DELETE` turns it off |
| 221 | (`disable_scim`). `get_sso` lists who is assigned under `scim`. Recorded as |
| 222 | `sso.scim_enabled`, `sso.scim_token_rotated`, `sso.scim_disabled`, |
| 223 | `sso.scim_user_provisioned`, `sso.scim_user_activated`, |
| 224 | `sso.scim_user_deactivated`, `sso.scim_user_deleted`, |
| 225 | `member.removed_by_scim` and `sso.scim_owner_kept`, with `scim` as the |
| 226 | actor. |