Skip to content
GitmoapdocsBeta
Guide· How do I…?

Single sign-on

Overview

Single sign-on connects your organisation to your own identity provider — Okta, Microsoft Entra ID, OneLogin, Ping, Google Workspace, or any SAML 2.0 or OpenID Connect provider — so your people reach Gitmoap with the credentials they already have. You prove you own an email domain, point Gitmoap at your provider, and decide who is allowed through: everybody on the domain, or only the people you name.

It also gives you the switch that closes the other door: turn on enforcement and anyone whose email is on one of your verified domains has to come through your provider, so a Gitmoap password stops being a second, unmanaged way into your code.

Before you start

  • Owner access on the top-level group.Single sign-on belongs to the organisation, not to a subgroup or a repository — every group call takes the id of the group at the top of your tree, and a subgroup is refused. Reading the connection is Owner-only too, because it carries issuer URLs, client ids and claim names; a Maintainer cannot see it.
  • A token with the right scope, if you are calling the API or the CLI rather than clicking: sso:read to read the connection, the domains and the pending invites, and sso:manage for everything else — every change, plus the connection test, which alters nothing but is gated alongside the changes. Scopes are fixed when you mint a token, so a script that only audits your domains should carry sso:read alone. Sign-in routing needs no token at all.
  • An application already created at your identity provider, pointed at your Gitmoap sign-in domain. For SAML you need the application's metadata URL, or its metadata document if your provider does not publish one. For OpenID Connect you need the issuer URL, the client id and the client secret.
  • The ability to add a DNS TXT recordto every email domain you intend to use. Ownership is proved with a TXT record, and nothing routes or links until the proof resolves.
  • An email claim your provider actually asserts.Gitmoap identifies a person by their verified email address, so the application must release it. If your provider names that claim something other than email, you say so in the attribute mapping.

How it works

The four parts

PartWhat it decidesHow many
Domainswhich email addresses belong to you, and therefore which sign-ins may be routed or linkedas many as you claim, each verified separately
The connectionwhich provider authenticates them, and how its claims map to a Gitmoap accountexactly one per organisation
Invites and automatic joiningwho is allowed to become a member, and with what roleone pending invite per email, plus one organisation-wide switch
Enforcementwhether a password is still an accepted way in for those addressesone switch

How a sign-in resolves

Everything turns on the domain of the email address a person types.

alice@acme.com
│
├─ acme.com claimed and verified by an organisation? ──── no ──▶ ordinary sign-in
│ yes
├─ that organisation's connection active? ─────────────── no ──▶ ordinary sign-in
│ yes
▼
your identity provider authenticates alice and asserts a verified email
│
├─ the email already has a Gitmoap account ────────▶ linked to it; membership granted
├─ no account, a pending invite for the email ─────▶ account created with the invite's role
├─ no account, automatic joining is on ────────────▶ account created with the default role
└─ no account, no invite, automatic joining off ───▶ sign-in refused at your provider's callback

Two properties of that flow are worth holding on to.

One person is one account, keyed by their verified email. When somebody who already signed up with alice@acme.com first comes through your provider, Gitmoap links the two rather than creating a second Alice. Her repositories, tokens, stars and history are all still hers; she gains membership of your organisation, and from then on both routes reach the same account.

Linking and automatic joining only ever happen inside a domain you have proved you own, and only for an address your provider marks as verified. That gate is not optional and cannot be relaxed: without it a misconfigured — or hostile — provider could assert somebody else's address and take over their account. It is also the reason an unverified domain silently does nothing rather than half working.

The connection's status

Configuring the connection registers your provider and wires it into the sign-in page, which takes a moment and can fail for reasons that live at your provider. The connection reports where it got to.

StatusMeaningWhat to do
provisioningregistration is in flightwait for the call to return; the status it reports is the outcome
activeregistered and routingnothing — verified domains reach your provider
failedregistration was refusedread last_provision_error, fix the application at your provider, and send the configuration again
disabledyou turned single sign-on offsend the configuration again to bring it back; your verified domains are still yours

Group membership from your provider

Your provider can also say which of its groups a person is in, and a team can follow one of those groups. Map the claim your provider sends groups in — attribute_mapping.groups on the connection — and link each team to a group's name, spelled exactly as the claim spells it. Sending the configuration is what applies a new mapping, so a connection set up before you mapped groups needs sending again. Then every sign-in through your provider puts the person on the linked teams whose group it names and takes them off the ones it does not; a sign-in whose claim names none of those groups takes them off all of them. Only your provider's word counts: the groups are read only for an account linked to your organisation's own provider, and only your provider can set them — nobody can write their own groups to put themselves on a team.

A place your provider confirms lasts for the connection's sync period, idp_sync_max_age_days: 30 days unless you set it, anywhere from 1 to 365. Someone who leaves a group but never signs in again drops off the team once the period passes. A groups claim of 2 048 characters or more may not have arrived whole, so it changes nothing at all rather than removing people on a partial list — and neither does a claim Gitmoap cannot read. Have your provider send only the groups your teams follow.

Sign-in sync learns about a change only when the person signs in, and the places it writes are not in your audit log one by one. For changes that take effect as they happen, including removing somebody who never signs in again, provision over SCIM. While your organisation has a live SCIM token, sign-in sync is off, so the two never disagree — and places on linked teams lapse at the end of the sync period, because nothing confirms them.

Domains are matched exactly

A verified acme.com routes alice@acme.com and nothing else. alice@eng.acme.com is a different domain: claim and verify it too, or those addresses go to the ordinary sign-in page. A verified domain belongs to one organisation only — no two organisations can hold the same one verified — which is what makes "this address belongs to that organisation" a fact rather than a preference.

Claims are not exclusive until verified

Claiming a domain only hands you the record that proves it. Until somebody verifies it, any number of organisations can claim the same domain, and none of them gains anything by it: an unverified claim routes no sign-in, links no account, shows on no organisation and unlocks no name. The first organisation to publish its record and verify owns the domain, and the other organisations' claims on it are removed. So a claim somebody else made on your domain cannot keep you out of it — verify it, and theirs goes.

A verified domain does more than route sign-ins. It shows on your organisation, as verified_domains on its details, to everyone who can see the organisation, which is how a reader tells your organisation from one with a similar name. And it unlocks your organisation's name if Gitmoap keeps that name for whoever owns the domain — see Take your reserved name.

How to

Claim an email domain

Claiming hands you the TXT record that proves you own the domain; it reserves nothing until you verify it. Claim every domain your people use email on, including the ones you only use for aliases.

Open the group, choose Settings, then Single sign-on. In the Domains panel choose Claim domain, type the domain in lower case, and Claim. The challenge appears in the row straight away: a record name beginning _gitmoap-challenge. and a value beginning gitmoap-domain-verification=. Copy both before you leave the screen.

Terminalbash
gitmoap sso claim-sso-domain --group-id <id> --domain <domain>
curlbash
curl -X POST https://api.gitmoap.example/groups/{group_id}/sso-domains \
-H "Authorization: Bearer $GITMOAP_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "domain": "<string>" }'

The record you add is a TXT record named _gitmoap-challenge.acme.com whose value is the whole gitmoap-domain-verification=… string. Most DNS providers want the name relative to the zone, so in the acme.com zone the name is _gitmoap-challenge alone — appending the zone twice is the single most common reason verification never succeeds.

Claiming a domain you already hold is safe: it returns the live challenge instead of a new one, so a lost record value is one call away rather than a reason to release and re-claim.

Verify a claimed domain

Verification resolves the TXT record and, if it matches, marks the domain verified. A verified domain is what routes sign-ins and what the linking gate checks, so nothing else on this page takes effect until at least one domain is verified. From that moment the domain also shows on your organisation and unlocks its reserved name, if it has one, and any other organisation's pending claim on the domain is removed.

In the Domains panel, find the pending row and choose Verify. A verified row shows the date it was checked; a row that fails stays pending, so you can fix the record and try again.

Terminalbash
gitmoap sso verify-sso-domain --group-id <id> --domain <id>
curlbash
curl -X POST https://api.gitmoap.example/groups/{group_id}/sso-domains/{domain}/verify \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Add the record first and give your DNS a minute. Verification reads what is published at that moment — it does not retry in the background, and it does not queue. A failed attempt does not mint a new challenge, so the record you already published is still the right one. If your connection is already active, verifying a domain starts routing its addresses to your provider straight away, with no further step; if sign-ins from it still meet a password form afterwards, send the configuration again, which re-registers every verified domain you hold.

Verifying a domain that is already verified is harmless and reports the same result.

See which domains you hold

The Domains panel lists every domain you have claimed, each with its state — pending, or verified with the date it was checked — and the date it was claimed.

Terminalbash
gitmoap sso list-sso-domains --group-id <id>
curlbash
curl -X GET https://api.gitmoap.example/groups/{group_id}/sso-domains \
-H "Authorization: Bearer $GITMOAP_TOKEN"

This is the list to check when a sign-in goes to the wrong place. A domain that is claimed but not verified routes nothing at all. The listing does not repeat the DNS challenge; claim the domain again to read that back.

Connect your identity provider

Configuring the connection registers your provider and attaches it to the sign-in page. Send the protocol and one matching configuration block: SAML with either a metadata URL or a metadata document, or OpenID Connect with an issuer URL and a client id. An OpenID Connect connection requests openid, email and profile unless you name your own set.

In the Identity provider panel choose Configure, pick SAML or OpenID Connect, and fill in the fields for the one you picked. Set the attribute mapping if your provider names the email claim something other than email, choose the role new members get, and set the two switches described below. Save registers the provider and reports the result; a failure shows the reason the registration was refused.

Terminalbash
gitmoap sso put-sso-connection --group-id <id> --protocol saml|oidc
curlbash
curl -X PUT https://api.gitmoap.example/groups/{group_id}/sso-connection \
-H "Authorization: Bearer $GITMOAP_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "protocol": "<one of oidc | saml>" }'

Four things about this call are easy to get wrong the first time.

It replaces the whole configuration rather than patching it. Every setting you want has to be present on every call — a switch you leave out goes back to its default, and a default role you leave out goes back to Reporter.

Send the OpenID Connect client secret every time. Leaving it out keeps the secret Gitmoap has stored, but the registration is rebuilt from the call you just made — so a call that omits the secret registers without one, and the connection can come back failed.

The secret is write-only. Reading the connection tells you whether a secret is set, never what it is. Store it where you can find it, or be ready to mint a new one at your provider.

Exactly one block, matching the protocol. A SAML connection carrying an OpenID Connect block is refused, and so is a SAML block carrying both a metadata URL and an inline document. Prefer the metadata URL where your provider offers one: it is refetched, so a certificate rotation at your provider needs nothing from you.

The same call carries the four settings that decide who is admitted and how.

SettingDefaultWhat it does
Automatic joiningoffOn, the first sign-in from a verified domain creates the account and grants membership. Off, somebody with no Gitmoap account is refused unless you invited them
EnforcementoffOn, a password sign-in from a verified-domain address is refused and that person must come through your provider. Off, people on your domains may still sign in with a Gitmoap password
Default roleReporterthe role a linked, invited or automatically created member joins with at the top-level group — one of the five built-in roles, or one your organisation defined. An invite may name a different role, but only for somebody who has no account yet. A role of your own that is the default, or that a pending invite gives, cannot be deleted until you change them
Sync period30 dayshow long a team place your provider confirmed at sign-in lasts without another sign-in confirming it, from 1 to 365 days. A value outside that range is refused with 400

Enforcement is checked when somebody signs in with a password, and nowhere else. Access tokens are unaffected, so a build that pushes with a token keeps working. That also means it is not a way to end anyone's access: it closes the password route for your verified domains and leaves memberships and tokens exactly as they are. Turn it on after the connection reads active and the test passes, because from that moment your provider is the only way in for those addresses.

Check the connection

The test performs three read-only checks and changes nothing: that a connection exists, that the provider registration is live, and that you have at least one verified domain. Run it after a change at your provider, and before you tell anyone to try signing in. It is gated with the changes rather than the reads, so a token carrying only sso:read cannot run it.

In the Identity provider panel choose Test connection. Each check is listed with a tick or a cross and the detail behind it, so a failure names which of the three is wrong rather than reporting a single unhappy result.

Terminalbash
gitmoap sso test-sso-connection --group-id <id>
curlbash
curl -X POST https://api.gitmoap.example/groups/{group_id}/sso-connection/test \
-H "Authorization: Bearer $GITMOAP_TOKEN"

A test that reports no verified domains is the usual explanation for a connection that looks configured and routes nobody.

Review the connection

The Identity provider panel shows the live configuration: protocol, status, the issuer or metadata URL, the attribute mapping, the two switches, the default role and the domains that are verified. Where a secret is stored, the panel says so without showing it.

Terminalbash
gitmoap sso get-sso-connection --group-id <id>
curlbash
curl -X GET https://api.gitmoap.example/groups/{group_id}/sso-connection \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Reading the connection is recorded in your audit log as a sensitive read, with the account that did it. That is intentional: this view is the closest thing your organisation has to a description of how it authenticates, and it is worth knowing who looked.

Invite somebody who has no account

An invite pre-authorizes one email address. When that person first signs in through your provider, the account is created, membership is granted with the invite's role, and the invite is consumed. Invites are the alternative to automatic joining: use them when you want membership to stay an explicit decision.

In the Invites panel choose Invite, type the email address, and optionally pick a role — leave it empty and the invite takes your connection's default. Invite adds the row immediately.

Terminalbash
gitmoap sso create-sso-invite --group-id <id> --email <email>
curlbash
curl -X POST https://api.gitmoap.example/groups/{group_id}/sso-invites \
-H "Authorization: Bearer $GITMOAP_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "email": "<string>" }'

An invite sends no mail. It is a standing permission, not an invitation letter: nothing reaches the address, and the person joins by signing in through your provider like everybody else. Tell them yourself that they can sign in.

An invite decides the role only for somebody who has no Gitmoap account. Somebody who already has one is linked and joins with the connection's default role whatever the invite says — and the invite is spent all the same. To give an existing account a different role, give it to them on the group's access list.

The address is matched exactly, capital letters included, against what your provider asserts — so type it the way the mailbox is actually spelled, in lower case unless you know your provider asserts otherwise.

Inviting an address only helps if its domain is one of yours and verified. An invite for a domain you do not hold sits there and never resolves, because the sign-in that would consume it never routes to you.

See who is waiting to join

The Invites panel lists every pending invite with the address, the role it grants, who created it and when. Rows disappear from the list as people sign in and use them.

Terminalbash
gitmoap sso list-sso-invites --group-id <id>
curlbash
curl -X GET https://api.gitmoap.example/groups/{group_id}/sso-invites \
-H "Authorization: Bearer $GITMOAP_TOKEN"

This list is also your cleanup worklist. An invite that has been pending since somebody's start date passed is usually a typo in the address, and it is holding a slot against the cap.

Revoke a pending invite

Revoking removes the pre-authorization. If the person has no Gitmoap account and automatic joining is off, their next sign-in is refused.

In the Invites panel, choose the bin icon at the end of the row and confirm. The row goes immediately.

Terminalbash
gitmoap sso delete-sso-invite --group-id <id> --email <id>
curlbash
curl -X DELETE https://api.gitmoap.example/groups/{group_id}/sso-invites/{email} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Revoking is addressed by the email, matched exactly as it was invited — take it from the listing rather than retyping it.

Revoking an invite somebody has already used takes nothing away: the invite was consumed at their first sign-in and their membership is a separate thing. Remove them from the group's members to end their access.

Route a sign-in to the right provider

Given an email address or a domain, this returns the URL to send somebody to so they land on your provider instead of a password form. It needs no credential, which is what lets your own sign-in page, a desktop tool or a CLI ask it before anyone has authenticated. A domain that does not route to an active connection answers plainly that it does not, and you fall back to the ordinary sign-in page.

The web sign-in page does this for you: type a work email address and it forwards you to your organisation's provider without offering a password field. Nothing to configure — it follows the domains you have verified.

Terminalbash
gitmoap sso resolve-sso --redirect-uri <redirect uri>
curlbash
curl -X GET https://api.gitmoap.example/auth/sso/resolve

The answer says whether the domain routes to single sign-on and, where it does, which protocol it speaks. Nothing else about the connection is exposed — no issuer, no client id, no claim mapping.

Release a domain

Warning

Releasing a domain revokes the proof, not the accounts. Sign-ins from that domain stop routing to your provider immediately, invites for addresses on it stop resolving, and linking and automatic joining stop applying to it. Everyone who already joined keeps their account, their membership and their access. Releasing also frees the domain for any other organisation to verify, and takes it off your organisation's details, so releasing one you still use is not something you can always undo. An organisation name the domain unlocked stays yours.

In the Domains panel, choose the bin icon at the end of the row and confirm. The confirmation names the domain; the release is immediate.

Terminalbash
gitmoap sso delete-sso-domain --group-id <id> --domain <id>
curlbash
curl -X DELETE https://api.gitmoap.example/groups/{group_id}/sso-domains/{domain} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Release a domain when you have genuinely stopped using it — a retired brand, a company you sold. To pause sign-ins without giving up the name, turn single sign-on off instead, which keeps every domain you have verified.

Turn single sign-on off

Warning

Turning single sign-on off deregisters your provider, deletes the stored client secret and any metadata document you uploaded, and stops all future federated sign-ins for your domains. It does not revoke anything: every account that was linked or created still exists, keeps its membership, its access level and its tokens, and — unless those people also have a Gitmoap password — they now have no way to sign in. Set up password access, or remove the memberships, before you rely on this as an off switch.

In the Identity provider panel choose Disable single sign-on and confirm. The panel then shows the connection as disabled; your claimed and verified domains stay exactly as they were.

Terminalbash
gitmoap sso delete-sso-connection --group-id <id>
curlbash
curl -X DELETE https://api.gitmoap.example/groups/{group_id}/sso-connection \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Configuring the connection again brings it back with no re-verification of your domains. Because the client secret was deleted, an OpenID Connect connection needs its secret supplied again.

Reference

12 endpoints

POST/groups/{group_id}/sso-domainssso:manage

Claim an email domain for the organization and obtain a DNS TXT verification challenge.

claim_sso_domainPOST /groups/{group_id}/sso-domainsOpen in the API reference

Token scope

sso:manage

Send

Path
  • group_iduuidThe organization's top-level group
Body
  • domainstring

Refuses

  • 400
  • 401
  • 403
  • 404
  • 409

Receive

  • domainstring
  • statuspendingverifiedWhether a claimed email domain has been verified. A pending domain has a DNS challenge to complete; only a verified domain links existing accounts or creates new ones at sign-in.
  • verificationobject3 fields
    • methodstringAlways dns_txt.
    • record_namestringThe TXT record name to create (_gitmoap-challenge.{domain}).
    • record_valuestringThe TXT record value (gitmoap-domain-verification=<token>).
POST/groups/{group_id}/sso-domains/{domain}/verifysso:manage

Check the domain's DNS TXT record and mark the domain verified.

verify_sso_domainPOST /groups/{group_id}/sso-domains/{domain}/verifyOpen in the API reference

Token scope

sso:manage

Send

Path
  • group_iduuidThe organization's top-level group
  • domainstringThe claimed domain

Refuses

  • 400The group is not an organization's top-level group
  • 401
  • 403
  • 404
  • 409
  • 501SSO is not configured on this deployment

Receive

  • created_atstring
  • domainstring
  • statuspendingverifiedWhether a claimed email domain has been verified. A pending domain has a DNS challenge to complete; only a verified domain links existing accounts or creates new ones at sign-in.
  • verified_atobjectoptional
GET/groups/{group_id}/sso-domainssso:read

List the organization's claimed domains and their verification status.

list_sso_domainsGET /groups/{group_id}/sso-domainsOpen in the API reference

Token scope

sso:read

Send

Path
  • group_iduuidThe organization's top-level group

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403
  • 404

Receive

  • domains[]object4 fields
    • created_atstring
    • domainstring
    • statuspendingverifiedWhether a claimed email domain has been verified. A pending domain has a DNS challenge to complete; only a verified domain links existing accounts or creates new ones at sign-in.
    • verified_atobjectoptional
DELETE/groups/{group_id}/sso-domains/{domain}sso:manage

Release a claimed domain.

delete_sso_domainDELETE /groups/{group_id}/sso-domains/{domain}Open in the API reference

Token scope

sso:manage

Send

Path
  • group_iduuidThe organization's top-level group
  • domainstringThe claimed domain

Refuses

  • 400The group is not an organization's top-level group
  • 401
  • 403
  • 404
  • 409The request conflicts with the resource's current state; `reason` says how
  • 501SSO is not configured on this deployment

Receive

No body.

PUT/groups/{group_id}/sso-connectionsso:manage

Create or replace the organization's single sign-on connection.

put_sso_connectionPUT /groups/{group_id}/sso-connectionOpen in the API reference

Token scope

sso:manage

Send

Path
  • group_iduuidThe organization's top-level group
Body
  • attribute_mappingobjectoptional
  • auto_provisionbooleanoptionalWhether to create an account for a person from a verified domain the first time they sign in. Defaults to false.
  • default_roleobjectoptional
  • enforcedbooleanoptionalWhether people from a verified domain must sign in through SSO. Defaults to false.
  • idp_sync_max_age_daysint32optionalDays, 1–365, a person's place on a team linked to an identity-provider group lasts without them signing in again to confirm it (default 30).
  • oidcobjectoptional
  • protocoloidcsamlThe IdP protocol an org's connection speaks.
  • samlobjectoptional

Refuses

  • 400
  • 401
  • 403
  • 404
  • 409
  • 501SSO is not configured on this deployment

Receive

  • attribute_mappingobject3 fields
    • emailstringThe claim or attribute carrying the person's email address. Defaults to email.
    • groupsobjectoptionalThe claim listing the person's groups at the identity provider, if any. Teams linked to those groups follow it.
    • usernameobjectoptionalThe claim a new account's username is derived from, if any.
  • auto_provisionboolean
  • cognito_idp_namestring
  • created_atstring
  • created_byobjectoptional
  • default_roleobject4 fields
    • builtinobjectoptional
    • customobjectoptional
    • namestringThe role's display name: a built-in name such as Developer, or the custom role's name.
    • rankdeveloperguestmaintainerownerreporter
  • enforcedboolean
  • idp_sync_max_age_daysint32Days a place on a team linked to an identity-provider group lasts without the person signing in again.
  • last_provision_errorobjectoptional
  • oidcobjectoptional
  • protocoloidcsamlThe IdP protocol an org's connection speaks.
  • root_group_iduuid
  • samlobjectoptional
  • statusactivedisabledfailedpendingprovisioningWhere an organization's SSO connection is in being set up. A connection left provisioning or failed is retried by saving the connection again.
  • updated_atstring
GET/groups/{group_id}/sso-connectionsso:read

Read the organization's single sign-on connection.

get_sso_connectionGET /groups/{group_id}/sso-connectionOpen in the API reference

Token scope

sso:read

Send

Path
  • group_iduuidThe organization's top-level group

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403
  • 404

Receive

The same shape put_sso_connection returns — 15 fields. Go to it

Show it here
  • attribute_mappingobject3 fields
    • emailstringThe claim or attribute carrying the person's email address. Defaults to email.
    • groupsobjectoptionalThe claim listing the person's groups at the identity provider, if any. Teams linked to those groups follow it.
    • usernameobjectoptionalThe claim a new account's username is derived from, if any.
  • auto_provisionboolean
  • cognito_idp_namestring
  • created_atstring
  • created_byobjectoptional
  • default_roleobject4 fields
    • builtinobjectoptional
    • customobjectoptional
    • namestringThe role's display name: a built-in name such as Developer, or the custom role's name.
    • rankdeveloperguestmaintainerownerreporter
  • enforcedboolean
  • idp_sync_max_age_daysint32Days a place on a team linked to an identity-provider group lasts without the person signing in again.
  • last_provision_errorobjectoptional
  • oidcobjectoptional
  • protocoloidcsamlThe IdP protocol an org's connection speaks.
  • root_group_iduuid
  • samlobjectoptional
  • statusactivedisabledfailedpendingprovisioningWhere an organization's SSO connection is in being set up. A connection left provisioning or failed is retried by saving the connection again.
  • updated_atstring
POST/groups/{group_id}/sso-connection/testsso:manage

Check the organization's single sign-on connection without changing it.

test_sso_connectionPOST /groups/{group_id}/sso-connection/testOpen in the API reference

Token scope

sso:manage

Send

Path
  • group_iduuidThe organization's top-level group

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403
  • 404
  • 409The request conflicts with the resource's current state; `reason` says how
  • 501SSO is not configured on this deployment

Receive

  • checks[]object3 fields
    • detailobjectoptional
    • namestring
    • okboolean
  • okboolean
DELETE/groups/{group_id}/sso-connectionsso:manage

Disable the organization's single sign-on.

delete_sso_connectionDELETE /groups/{group_id}/sso-connectionOpen in the API reference

Token scope

sso:manage

Send

Path
  • group_iduuidThe organization's top-level group

Refuses

  • 400The group is not an organization's top-level group
  • 401
  • 403
  • 404
  • 409The request conflicts with the resource's current state; `reason` says how
  • 501SSO is not configured on this deployment

Receive

No body.

POST/groups/{group_id}/sso-invitessso:manage

Invite an email address to join the organization at its next single sign-on.

create_sso_invitePOST /groups/{group_id}/sso-invitesOpen in the API reference

Token scope

sso:manage

Counts against

sso_invites_per_scope

Send

Path
  • group_iduuidThe organization's top-level group
Body
  • emailstring
  • roleobjectoptional

Refuses

  • 400
  • 401
  • 403
  • 404
  • 409
  • 429

Receive

  • created_atstring
  • emailstring
  • invited_byobjectoptional
  • roleobject4 fields
    • builtinobjectoptional
    • customobjectoptional
    • namestringThe role's display name: a built-in name such as Developer, or the custom role's name.
    • rankdeveloperguestmaintainerownerreporter
GET/groups/{group_id}/sso-invitessso:read

List the organization's pending single sign-on invites.

list_sso_invitesGET /groups/{group_id}/sso-invitesOpen in the API reference

Token scope

sso:read

Send

Path
  • group_iduuidThe organization's top-level group

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403
  • 404

Receive

  • invites[]object7 fields
    • created_atstring
    • emailstring
    • invited_byobjectoptional
    • roleobject4 fields
      • builtinobjectoptional
      • customobjectoptional
      • namestringThe role's display name: a built-in name such as Developer, or the custom role's name.
      • rankdeveloperguestmaintainerownerreporter
DELETE/groups/{group_id}/sso-invites/{email}sso:manage

Revoke a pending single sign-on invite.

delete_sso_inviteDELETE /groups/{group_id}/sso-invites/{email}Open in the API reference

Token scope

sso:manage

Send

Path
  • group_iduuidThe organization's top-level group
  • emailstringThe invited email

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401
  • 403
  • 404
  • 409The request conflicts with the resource's current state; `reason` says how

Receive

No body.

GET/auth/sso/resolvepublic

Route an email address to its organization's single sign-on.

resolve_ssoGET /auth/sso/resolveOpen in the API reference

Token scope

None — this one is public

Send

Query
  • emailstringoptionalCaller's email (domain is extracted)
  • domainstringoptionalExplicit domain hint
  • redirect_uristringClient callback URL
  • statestringoptionalOpaque CSRF state

Refuses

  • 400
  • 501SSO login routing is not configured on this deployment

Receive

  • authorize_urlobjectoptional
  • protocolobjectoptional
  • ssoboolean

Limits

A thousand pending invites per organisation. The number is the same on every plan, and it counts invites that are still waiting: an invite consumed at somebody's first sign-in, or revoked, frees its slot the moment it goes.

The thousand-and-first is refused with 429, naming the invite limit. Nothing is created by the refused call, so a retry once you have freed a slot behaves exactly as the first attempt would have. If you are anywhere near it, the fix is almost never a bigger number: revoke the invites that have been pending for months, and turn automatic joining on so that people on a verified domain need no invite at all.

Joining consumes a member slot on your top-level group, and that cap does depend on your plan: 50 members on Free, 200 on Pro, 1 000 on Team and 5 000 on Enterprise. This one bites in an unusual way. When the group is full, the sign-in still succeeds — your provider authenticated them and the account is real — but membership is not granted, so the person arrives able to sign in and unable to see anything. The group's usage and limits view shows the live count against the cap, and it is worth checking before a rollout rather than after. Where you need a higher ceiling, request a limit increase on the group; it is an Owner-level request and it is reviewed.

Troubleshooting

400 BAD_REQUEST, "SSO can only be configured on a root group"
Cause
You sent a subgroup's id to a call that changes something
What to do
Use the id of the group at the top of your tree. Single sign-on is one configuration for the whole organisation and does not exist on a subgroup
400 BAD_REQUESTnaming the domain
Cause
The domain is not a lower-case fully-qualified name — an upper-case letter, no dot, a leading or trailing dot, an underscore, or a label over 63 characters
What to do
Send it lower case and fully qualified: acme.com, not ACME.com, acme or acme.com.
400 BAD_REQUESTabout the protocol and the configuration block
Cause
The block does not match the protocol — the wrong block for the protocol, no block at all, both SAML metadata fields at once, neither of them, or an OpenID Connect block missing the issuer URL or client id
What to do
Send exactly one block, matching the protocol, with exactly one SAML metadata source. The message names which of those it was
400 BAD_REQUESTnaming an unknown field
Cause
The body carries a field name the API does not define — usually a misspelling, or your provider's own vocabulary in place of Gitmoap's
What to do
Check the spelling against the request shape on the card above. Unknown fields are rejected rather than ignored, so a typo never silently drops a setting
400 BAD_REQUESTnaming the email
Cause
The address is not a well-formed one — no @, an empty side of it, whitespace, or over 254 characters
What to do
Send one plain address per invite. Distribution lists and comma-separated batches are not addresses; invite each person separately
400 BAD_REQUESTon sign-in routing
Cause
Neither an email nor a domain was supplied, the callback URL is missing, or the value given is not a well-formed address or domain
What to do
Send one of email or domain, and always a callback URL. An email is enough — the domain is taken from it
400 BAD_REQUESTon any call, mentioning an id
Cause
The group id in the URL is not a well-formed id — often a group path pasted where an id belongs
What to do
Use the id. The CLI accepts a path for --group-id and resolves it for you
401 UNAUTHORIZED
Cause
No credential was sent, or the token has expired or been revoked
What to do
Sign in again, or mint a new token. Sign-in routing is the one call here that needs no credential
403 FORBIDDENon a group you are a member of
Cause
Your access level is below Owner. Every group call on this page is Owner-only, reads included
What to do
Ask an Owner of the top-level group to make the change, or to raise your level. Maintainer is not enough, deliberately: this configuration is credential-adjacent
403 INSUFFICIENT_SCOPE
Cause
The token is valid but was minted without the scope this call needs
What to do
Mint a new token carrying sso:read for the reads, or sso:manage for the changes and the connection test. Scopes are fixed at mint time and cannot be added afterwards
404 NOT_FOUNDon a group you are sure exists
Cause
The group is private and you are not a member, the id is wrong, or you sent a subgroup id to a read
What to do
A private group answers 404 rather than 403 so that its existence stays private. Confirm your membership, then confirm you are using the top-level group's id
404 NOT_FOUND, "no SSO connection configured"
Cause
You are reading, testing or disabling a connection that has never been configured on this group — often because it was configured on a different top-level group
What to do
Configure the connection first. If you believe one already exists, check which top-level group it was set up on
404 NOT_FOUND, "no pending domain challenge"
Cause
You are verifying a domain this organisation has not claimed — usually a spelling difference, or a claim made on a different group. It also happens when another organisation verified the domain first: its verification removes every other organisation's pending claim on the domain
What to do
List your domains and verify one from that list. Claim it first if it is missing. If claiming it again is refused because the domain is verified by another organisation, see the row for that refusal below
404 NOT_FOUNDon releasing a domain or revoking an invite
Cause
The row is already gone, or belongs to another organisation
What to do
List the domains or the invites and act on a row from that list. Nothing was changed by the refused call
409 CONFLICTon claiming, "domain already verified by another organization"
Cause
Another organisation has verified that domain. A verified domain belongs to exactly one organisation
What to do
Find out who inside your company already set this up — the usual answer is a second organisation created by mistake. Where it is genuinely somebody else, contact support; releasing it is theirs to do
409 CONFLICTon verify, "another organization verified this domain first"
Cause
Another organisation also claimed the domain and verified it before you did
What to do
Find out who inside your company verified it. If the domain is yours and the other organisation is not, contact support. Your pending claim is removed, so the domain no longer appears in your list
409 CONFLICTon verify
Cause
The TXT record was not found at the challenge name, or it does not contain the challenge value
What to do
Check the record name resolves — dig +short TXT _gitmoap-challenge.acme.com — and that the value is the complete gitmoap-domain-verification=… string. Then verify again
409 CONFLICT, "an SSO provisioning is already in progress"
Cause
A previous configuration attempt did not finish, leaving the connection mid-registration
What to do
Read the connection to confirm the status, then disable single sign-on and configure it again. Disabling is accepted in any state and clears the way
409 CONFLICTon inviting
Cause
A pending invite for that address already exists
What to do
Nothing to do — the address is already pre-authorized. To change the level it grants, revoke the invite and create it again
429 TOO_MANY_REQUESTSon inviting
Cause
A thousand invites are already pending for the organisation
What to do
Revoke the stale ones, or turn automatic joining on so people on a verified domain need no invite. Nothing was created by the refused call
501 NOT_IMPLEMENTED
Cause
Single sign-on is unavailable on Gitmoap's side, so there is nothing to register a provider with. It answers the calls that have to reach it — configuring, testing and disabling the connection, verifying and releasing a domain, and sign-in routing — while reading the connection, listing and claiming domains, and the invite calls all keep working
What to do
Contact support@gitmoap.com with the message, which names what is missing; nothing you can change on the group makes these calls work
502 BAD_GATEWAYon verify
Cause
The DNS lookup itself failed rather than returning the wrong answer
What to do
Retry. If it repeats, check the zone answers public queries at all — a name served only inside your network cannot be resolved from outside it
The connection saves but reports failed
Cause
The registration was refused — bad metadata, an unreachable issuer, a wrong client id, or an OpenID Connect call that left the client secret out
What to do
Read last_provision_error on the connection: it carries the reason given. Fix the application at your provider, then send the whole configuration again, client secret included. Nothing routes while the status is failed
Configuring with an inline SAML document fails with a server error while a metadata URL works
Cause
Storage for uploaded metadata documents is unavailable on Gitmoap's side, so the document cannot be stored
What to do
Use the metadata URL your provider publishes. It is the better option anyway — it is refetched, so certificate rotations need nothing from you
A work email still lands on the password form
Cause
The domain is claimed but not verified, the connection is not active, or the address is on a subdomain you have not claimed
What to do
List your domains and read the connection. alice@eng.acme.com needs eng.acme.com claimed and verified in its own right
Sign-in reaches the hosted sign-in page and stops there with an unknown-provider error, although the domain reads verified
Cause
The verified domain never reached the provider registration — the refresh that runs after a verify or a release is best-effort and can be lost
What to do
Send the configuration again. It re-registers every domain that stands verified at that moment, which is also the repair after a release
Sign-in through the provider is refused with no Gitmoap account created
Cause
The person has no account, has no pending invite, and automatic joining is off
What to do
Invite the address, or turn automatic joining on. Both take effect on their next attempt with no further step
Sign-in through the provider is refused although the person is invited
Cause
Your provider did not assert the email as verified, or asserted it on a domain you have not verified
What to do
Check the application releases a verified email claim, and that the mapping names the right claim. Linking and automatic joining apply only to verified addresses on verified domains
The invite is still listed after somebody signed in, and they joined at the wrong level
Cause
The invited address differs from the one your provider asserts — most often in capitalisation. Matching is exact, so the invite was never found
What to do
Revoke the stale invite and create it spelled exactly as your provider asserts it. Then set the person's level on the group's member list, which is where an already-joined member's level lives
Somebody signs in successfully and can open nothing private
Cause
Membership was not granted — the group is at its member cap — or the default access level is Guest, which shows that private groups and repositories exist and reads none of them
What to do
Check the group's usage and limits view for the member count, free a slot or request an increase, then add them as a member. Raise the default access level so later arrivals land correctly
People still sign in with a Gitmoap password after you turned enforcement on
Cause
Their address is not on a verified domain, or the connection is not active. Enforcement gates only verified-domain addresses of an active connection
What to do
Verify the domain they actually use and confirm the connection is active. Addresses outside your domains — contractors on their own mail — keep signing in with a password by design
Somebody removed at your provider still has access
Cause
Removing a person there stops them federating; without SCIM it does not touch the Gitmoap account, the membership or any token they minted
What to do
Provision over SCIM, which removes their access the moment your provider deactivates them. Without it, remove their access to the group and revoke their tokens
People stay on a linked team after leaving the group at your provider
Cause
They have not signed in since, and their place lasts for the sync period
What to do
Shorten the sync period, or provision over SCIM for immediate changes
Two people end up sharing one account
Cause
Both addresses are aliases delivering to the same mailbox, and your provider asserted the same email for both
What to do
Give each person a distinct primary address at your provider. An account is identified by its email, so one address is one person

Examples

Setting up an OpenID Connect provider end to end

shellscript
# 1. Claim the domain. The response carries the TXT record to publish.
gitmoap sso claim-sso-domain --group-id acme --domain acme.com
# 2. Publish the record in the acme.com zone, then confirm it resolves.
dig +short TXT _gitmoap-challenge.acme.com
# 3. Verify. This is what makes the domain route and what opens the linking gate.
gitmoap sso verify-sso-domain --group-id acme --domain acme.com
# 4. Register the provider. Everyone on a verified domain may join, as a Developer.
gitmoap sso put-sso-connection --group-id acme \
--protocol oidc \
--oidc '{"issuer_url":"https://acme.example-idp.com","client_id":"0oa1example","client_secret":"<client-secret>"}' \
--default-member-access-level developer \
--auto-provision true
# 5. Confirm all three checks pass before telling anyone to try.
gitmoap sso test-sso-connection --group-id acme

--group-id takes the group's id or its path. <client-secret> is the secret from the application you created at your provider; it is stored write-only and never read back.

A SAML provider, invite-only, with passwords blocked

shellscript
gitmoap sso put-sso-connection --group-id acme \
--protocol saml \
--saml '{"metadata_url":"https://acme.example-idp.com/app/exk1example/sso/saml/metadata"}' \
--attribute-mapping '{"email":"email","username":"preferred_username"}' \
--default-member-access-level reporter \
--enforced true
gitmoap sso create-sso-invite --group-id acme --email alice@acme.com --access-level maintainer
gitmoap sso create-sso-invite --group-id acme --email bob@acme.com

Automatic joining is left off, so only the two invited addresses can create an account. Enforcement is on, so everyone else on acme.com who tries a password is refused. Bob joins as a Reporter — the connection default — and Alice as a Maintainer.

Clearing out invites that were never used

shellscript
gitmoap sso list-sso-invites --group-id acme --output json \
| jq -r '.invites[] | select(.created_at < "2026-06-01") | .email' \
| while read -r email; do
gitmoap sso delete-sso-invite --group-id acme --email "$email"
done

Anything still pending months after a start date is usually a mistyped address, and each one holds a slot against the invite cap.

FAQ

Does connecting a provider stop passwords working?

Only if you turn enforcement on. Left off, single sign-on is an additional way in and every existing password keeps working. Turn it on once you have watched real people sign in through your provider.

What happens to somebody who already has a Gitmoap account?

Their first sign-in through your provider links the two, and both routes then reach the same account with the same repositories, tokens and history. There is no migration and nothing to move.

Do subdomains inherit a verified domain?

No. Matching is exact, so acme.com does nothing for eng.acme.com. Claim and verify each domain your people actually receive mail on.

Can I read the client secret back?

No. Any read tells you whether a secret is stored, never what it is. Keep your own copy: an OpenID Connect provider is registered from the call you make, so every configuration call needs the secret in it.

Can two organisations use the same domain?

Not once it is verified. Several organisations can claim a domain, but the first to verify it owns it, the others' claims are removed, and a later claim is refused. If two of your own groups need it, decide which one is the organisation and set single sign-on up there.

What access level do people get?

The connection's default, unless an invite names another one, and an invite's level applies only to somebody who has no account yet. Change anyone's level afterwards through the group's member list, the same as any other member.

Does removing somebody at my provider remove them from Gitmoap?

With SCIM, yes: your provider's deactivation takes away their access to your organisation within the same request. Without it, removal stops them federating, and that is all — remove their access to the group and revoke their tokens to end it.

If I remove somebody's membership, does signing in again bring it back?

No. The organization membership is granted when a person first arrives — linked or created by single sign-on, or holding one of your invites — and never merely because they signed in again. A removal stays removed until you invite them back.

What happens to my domains if I disable single sign-on?

Verified ones stay verified, keep showing on your organisation, and nobody else can take them. Configuring a provider again picks up exactly where you left off, with no second round of DNS.

  • SCIM provisioningyour provider adding, grouping and removing people as it changes
  • Teamsteams that follow a group at your provider
  • Groupsthe top-level group single sign-on attaches to, the organisation name a verified domain unlocks, its member list, and the usage view that shows your live member count against the cap
  • Access tokensminting a token that carries sso:read or sso:manage, and revoking the tokens of somebody who has left
  • Your accountthe profile, username and email that identify a person, and what linking joins together
  • Audit logwho configured, tested or disabled the connection, who claimed or released a domain, and who read the configuration
  • Billing and plansthe member cap per plan, and requesting an increase when your organisation outgrows it
  • Service accountsthe non-human identities your automation signs in as, which federation never covers