instructure/canvas-lms · error · OAuthValidationError
User is from unacceptable issuer
Error message
User is from unacceptable issuer %{issuer}. What it means
Raised by Canvas's Microsoft authentication provider during OAuth login when the admin configured the provider to allow 'guests' tenants plus specific tenant IDs, but the ID token's issuer (iss) does not match https://login.microsoftonline.com/<tenant>/v2.0 for any allowed tenant. It is an OAuthValidationError meaning the user authenticates successfully with Microsoft but is not from an acceptable tenant.
Solutions
- Add the exact tenant ID(s) whose users should be allowed to the provider's allowed_tenants config so the issuer URL matches.
- Verify the configured tenant IDs match the 'tid' claim shown in the rejected issuer URL in the error message.
- If any Microsoft account should be allowed, set skip_tenant_verification (or include 'common') in the provider settings.
- Confirm the issuer version: the check expects the /v2.0 endpoint; ensure the provider uses the v2.0 endpoints.
Example fix
# before settings["microsoft_tenant"] = "guests,contoso.onmicrosoft.com" # after (use the tenant GUID or ensure issuer matches) settings["allowed_tenants"] = "guests,00000000-0000-0000-0000-000000000000"
Defensive patterns
Strategy: validation
Validate before calling
iss = id_token["iss"]
allowed = provider_settings["allowed_tenants"].split(",")
unless allowed.empty? || allowed.include?("common") || provider_settings["skip_tenant_verification"]
raise "unacceptable issuer #{iss}" if allowed.include?("guests") && !allowed.grep_v("guests").any? { |t| iss == "https://login.microsoftonline.com/#{t}/v2.0" }
end Try / catch
begin
user.unique_id
rescue OAuthValidationError => e
Rails.logger.warn("Microsoft tenant rejected: #{e.message}")
redirect_to login_path, alert: "Your Microsoft account is not from an allowed tenant."
end Prevention
- Always use Azure tenant GUIDs in the allowed tenants list
- Include 'guests' plus every guest home tenant you expect
- Test login with a personal Microsoft account when 'common' is not set
- Check the issuer URL version (/v2.0) matches your endpoints
When it happens
Trigger: unique_id runs after Microsoft callback: allowed_tenants is non-empty, does not include 'common', skip_tenant_verification is off, and 'guests' was in the configured tenants list; id_token['iss'] (e.g. https://login.microsoftonline.com/9188040d-.../v2.0 for personal MSA accounts) matches none of the allowed tenant issuer URLs.
Common situations: Admin adds 'guests' to allowed tenants expecting guest accounts to pass, but the user's issuer is a tenant not explicitly listed (personal Microsoft accounts or guest home tenants). Azure AD tenant ID changes or issuer URL version differs (v2.0 vs v1.0). Typos in tenant IDs in the allowed list.
Related errors
- User is from unacceptable tenant
- Failed to decode OpenID Connect id_token: #
- User is from unacceptable domain
- An object of type was hidden due to insufficient scopes on…
- authorization_code_not_supplied
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/d83e169ca7c0ff25.
Report an issue: GitHub.
Appendix: source
Thrown at app/models/authentication_provider/microsoft.rb:103
end
def self.validate_issuer?
false
end
def login_attribute
raw_login_attribute || "tid+oid"
end
def unique_id(token)
id_token = claims(token)
allowed_tenants = mapped_allowed_tenants
if allowed_tenants.empty? || allowed_tenants.include?("common") || settings["skip_tenant_verification"]
# allow anyone
elsif allowed_tenants.delete("guests")
# just check the issuer
unless allowed_tenants.find { |tenant| id_token["iss"] == "https://login.microsoftonline.com/#{tenant}/v2.0" }
raise OAuthValidationError, t("User is from unacceptable issuer %{issuer}.", issuer: id_token["iss"].inspect)
end
elsif !allowed_tenants.include?(id_token["tid"])
raise OAuthValidationError, t("User is from unacceptable tenant %{tenant}.", tenant: id_token["tid"].inspect)
end
ids = id_token.as_json
ids["tid+oid"] = "#{ids["tid"]}##{ids["oid"]}" if ids["tid"] && ids["oid"]
ids.slice("tid", *self.class.login_attributes)
end
# always process through the multi-valued setter
def tenant=(value)
self.tenants = value
end
def tenants=(value)
value = value.split(",") if value.is_a?(String)
value = value.filter_map(&:strip).uniqView on GitHub (pinned to 1c9f0bb801)