dromara/Sa-Token · error · SaOAuth2ClientModelScopeException

30112

30112

Error message

该 client 暂未签约 scope: 

What it means

Thrown by SaOAuth2Template.checkContractScope: the client is valid but one of the requested scopes is not in its contractScopes list. An empty/absent scope list passes silently; any non-empty scope must be contracted. Thrown as SaOAuth2ClientModelScopeException with clientId and the offending scope. Error code 30112.

Source

Thrown at sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java:134

	 * @return /
	 */
	public SaClientModel checkContractScope(String clientId, List<String> scopes) {
		return checkContractScope(checkClientModel(clientId), scopes);
	}

	/**
	 * 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常
	 * @param cm 应用
	 * @param scopes 权限列表
	 * @return /
	 */
	public SaClientModel checkContractScope(SaClientModel cm, List<String> scopes) {
		if(SaFoxUtil.isEmptyList(scopes)) {
			return cm;
		}
		for (String scope : scopes) {
			if(! cm.contractScopes.contains(scope)) {
				throw new SaOAuth2ClientModelScopeException("该 client 暂未签约 scope: " + scope)
						.setClientId(cm.clientId)
						.setScope(scope)
						.setCode(SaOAuth2ErrorCode.CODE_30112);
			}
		}
		return cm;
	}

	// --------- redirect_uri 相关

	/**
	 * 校验:该 Client 使用指定 url 作为回调地址,是否合法
	 * @param clientId 应用id
	 * @param url 指定url
	 */
	public void checkRedirectUri(String clientId, String url) {
		// 1、是否是一个有效的url
		if( ! SaFoxUtil.isUrl(url)) {

View on GitHub (pinned to ac2c7f6e94)

Solutions

  1. Add the missing scope to the client's contractScopes registration (setContractScopes)
  2. Or trim the requested scope down to what the client has contracted
  3. Keep scope names in a single shared constant list so client code and registration cannot drift

Example fix

// before
new SaClientModel().setClientId("1001")
    .setContractScopes(Collections.singletonList("getuserinfo"));
// request: scope=getuserinfo,orders  -> throws 30112

// after
new SaClientModel().setClientId("1001")
    .setContractScopes(Arrays.asList("getuserinfo", "orders"));
Defensive patterns

Strategy: validation

Validate before calling

SaClientModel cm = oauth2Template.checkClientModel(clientId);
for(String s : requestedScopes) {
    if(!cm.getContractScopes().contains(s)) {
        throw new IllegalArgumentException("scope not contracted: " + s);
    }
}

Try / catch

catch(SaOAuth2ClientModelScopeException e) {
    if("30112".equals(e.getCode())) return badRequest("scope not allowed: " + e.getScope());
}

Prevention

When it happens

Trigger: Authorize or token request with scope=getuserinfo,orders while the SaClientModel only contracts [getuserinfo]; scope string with a stray value (typo, extra separator producing an empty/odd token); frontend requests an ever-growing scope list after new features ship.

Common situations: New API scope added to the client app but the server-side client registration was not updated; different environments register different contractScopes; scope string parsing splits on unexpected separators.

Related errors


AI-assisted analysis of dromara/Sa-Token@ac2c7f6e94 (2026-08-14). Data as JSON: /api/errors/d25c3a2f072efacd. Report an issue: GitHub.