Spring Oauth2-Authorization-Server client_secret_jwt

蓟浩旷
2023-12-01

Spring Oauth2-Authorization-Server client_secret_jwt过程

基于 spring-security-oauth2-authorization-server 0.2.3

客户端使用 client_id和client_secret 进行认证,且使用Jwt加密请求时,加密的方式 JwsAlgorithm有:

  • MacAlgorithm
  • SignatureAlgorithm

JWT 签名算法

  • MacAlgorithm: MAC(Message Authentication Codes),是一种消息摘要算法,也叫消息认证码算法。. 这种算法的核心是基于秘钥的散列函数。可以理解为,MAC算法,是MD5算法和SHA算法的升级版,是在这两种算法的基础上,又加入了秘钥的概念,更加安全。所以,有时候又叫MAC算法为HMAC算法(keyed-Hash Message Authentication Codes),即含有秘钥的散列算法
  • SignatureAlgorithm: 采用非对称加密,用private key 进行加密,用公钥验签

client_secret_jwt 配置

@Bean
public RegisteredClientRepository jdbcRegisteredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
    RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
        // 客户端id 需要唯一
        .clientId("apple")
        // 客户端密码
        .clientSecret(passwordEncoder.encode("apple_secret"))
        // 默认基于 basic 的方式 认证 client_id和client_secret
        // Base64.encode(string(client_id:clientSecret))
        //!!必须
        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
        // 授权码
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        // 刷新token
        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
        // 客户端模式
        .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
        // 密码模式, 目前没有实现
        .authorizationGrantType(AuthorizationGrantType.PASSWORD)
        //废弃
        .authorizationGrantType(AuthorizationGrantType.IMPLICIT)
        // 重定向url
        .redirectUri("https://www.baidu.com")
        // 客户端申请的作用域,也可以理解这个客户端申请访问用户的哪些信息,比如:获取用户信息,获取用户照片等
        .scope("user_info")
        .scope("user_photo")
        .clientSettings(ClientSettings.builder()
                        //必须!!
                        .tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256)
                        .requireAuthorizationConsent(true)
                        .build())
        .tokenSettings(TokenSettings.builder()
                       //使用透明方式,默认是 OAuth2TokenFormat SELF_CONTAINED
                       .accessTokenFormat(OAuth2TokenFormat.REFERENCE)
                       // 授权码的有效期
                       .accessTokenTimeToLive(Duration.ofHours(1))
                       // 刷新token的有效期
                       .refreshTokenTimeToLive(Duration.ofDays(3))
                       .reuseRefreshTokens(true).build())
        .build();

    JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
    jdbcRegisteredClientRepository.save(registeredClient);
    return jdbcRegisteredClientRepository;
}

client_secret_jwt 三个要点:

  • clientAuthenticationMethod 为 ClientAuthenticationMethod.CLIENT_SECRET_JWT
  • clientSettings.tokenEndpointAuthenticationSigningAlgorithm 为 MacAlgorithm 算法
  • tokenSettings.accessTokenFormat 为 OAuth2TokenFormat.REFERENCE

获取 token

参数参数值
scope
grant_type授权类型:client_credentials
client_assertion_typejwt type: urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertionJwt token 值
client_idclient_id

client_assertion 参数生成 jwt 请求参数:

public static String gen() throws Exception {
    String clientId = "apple";
    // 我这里使用 BCryptPasswordEncoder 加密,和 授权服务器中 保存 RegisteredClient 的secret 一致!!
    String clientSecret = "apple_secret";
    //algorithm 必须是 HMACSHA256
    SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),
                                                    "HmacSHA256");
    JWSSigner signer = new MACSigner(secretKeySpec);
    //subject, issuer, audience, expirationTime 这四个参数是必须的
    //服务器那边会校验
    JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
        //主题
        .subject(clientId)
        //签发人
        .issuer(clientId)
        //受众 必填
        .audience("http://localhost:9000")
        //过期时间
        .expirationTime(new Date(System.currentTimeMillis() + 60 * 60 * 60 * 1000))
        .build();

    SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
    signedJWT.sign(signer);
    String token = signedJWT.serialize();
    System.out.println(token);
    return token;
}

如生成:

yJhbGciOiJIUzI1NiJ9.eyJpc3MioiaHR0cDpcL1wvbG9jYWxob3N0OjkwMDAiLCJleHAiOjE2NTE3ODMyNzV9.rpVurCOh

请求 token:

curl --location --request POST 'http://127.0.0.1:9000/oauth2/token?scope=user_info&grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJhcHBsZSIsInN1YiI6ImFwcGxlIiwiYXVkIjoiaHR0cDpcL1wvbG9jYWxob3N0OjkwMDAiLCJleHAiOjE2NTE3ODMyNzV9.rpVurCOhnxqJy83jLGwmxwMdAxvTiB76cvfmeYEryX4=&client_id=apple'
{
    "access_token": "oSORyWslthM9irNVnLxHB8bX....4P",
    "scope": "user_info",
    "token_type": "Bearer",
    "expires_in": 3599
}

client_secret_jwt 请求流程

  • OAuth2ClientAuthenticationFilter: 拦截 client_assertion
    • JwtClientAssertionAuthenticationConverter 拦截 client_assertion 并解析, 委托给 JwtClientAssertionAuthenticationProvider
    • JwtClientAssertionAuthenticationProvider
      • ClientAuthenticationMethod 必须为 urn:ietf:params:oauth:client-assertion-type:jwt-bearer
      • 根据 client_id 查询 RegisteredClient
      • 创建 JwtDecoder, 并解析 jwt, 这里很重要,看源码
      • 认证成功
  • OAuth2TokenEndpointFilter: 拦截 /oauth2/token
    • 委托 OAuth2ClientCredentialsAuthenticationConverter 解析 OAuth2ClientCredentialsAuthenticationToken
    • 委托 OAuth2ClientCredentialsAuthenticationProvider 进行认证,包括 scope等等
    • 生成 token 返回

源码解析

文章开头要求 client_secret_jwt 要遵守的三个点,原因是什么呢?

public final class JwtClientAssertionAuthenticationProvider implements AuthenticationProvider {
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		OAuth2ClientAuthenticationToken clientAuthentication =
				(OAuth2ClientAuthenticationToken) authentication;

		if (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {
			return null;
		}

		String clientId = clientAuthentication.getPrincipal().toString();
		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
		if (registeredClient == null) {
			throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
		}

		if (!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT) &&
				!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
			throwInvalidClient("authentication_method");
		}

		if (clientAuthentication.getCredentials() == null) {
			throwInvalidClient("credentials");
		}

		Jwt jwtAssertion = null;
		JwtDecoder jwtDecoder = this.jwtClientAssertionDecoderFactory.createDecoder(registeredClient);
		try {
			jwtAssertion = jwtDecoder.decode(clientAuthentication.getCredentials().toString());
		} catch (JwtException ex) {
			throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION, ex);
		}

		// Validate the "code_verifier" parameter for the confidential client, if available
		this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);

		ClientAuthenticationMethod clientAuthenticationMethod =
				registeredClient.getClientSettings()
            .getTokenEndpointAuthenticationSigningAlgorithm() instanceof SignatureAlgorithm ?
						ClientAuthenticationMethod.PRIVATE_KEY_JWT :
						ClientAuthenticationMethod.CLIENT_SECRET_JWT;

		return new OAuth2ClientAuthenticationToken(registeredClient, clientAuthenticationMethod, jwtAssertion);
	}

    
}

看 jwtClientAssertionDecoderFactory.createDecoder 方法

static {
    Map<JwsAlgorithm, String> mappings = new HashMap<>();
    mappings.put(MacAlgorithm.HS256, "HmacSHA256");
    mappings.put(MacAlgorithm.HS384, "HmacSHA384");
    mappings.put(MacAlgorithm.HS512, "HmacSHA512");
    JCA_ALGORITHM_MAPPINGS = Collections.unmodifiableMap(mappings);
}

@Override
public JwtDecoder createDecoder(RegisteredClient registeredClient) {
    Assert.notNull(registeredClient, "registeredClient cannot be null");
    return this.jwtDecoders.computeIfAbsent(registeredClient.getId(), (key) -> {
        NimbusJwtDecoder jwtDecoder = buildDecoder(registeredClient);
        jwtDecoder.setJwtValidator(createJwtValidator(registeredClient));
        return jwtDecoder;
    });
}

private static NimbusJwtDecoder buildDecoder(RegisteredClient registeredClient) {
    JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm();
    if (jwsAlgorithm instanceof SignatureAlgorithm) {
        String jwkSetUrl = registeredClient.getClientSettings().getJwkSetUrl();
        if (!StringUtils.hasText(jwkSetUrl)) {
            OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
                                                      JWT_CLIENT_AUTHENTICATION_ERROR_URI);
            throw new OAuth2AuthenticationException(oauth2Error);
        }
        return NimbusJwtDecoder.withJwkSetUri(jwkSetUrl).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm).build();
    }
    if (jwsAlgorithm instanceof MacAlgorithm) {
        String clientSecret = registeredClient.getClientSecret();
        if (!StringUtils.hasText(clientSecret)) {
            OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT)
            throw new OAuth2AuthenticationException(oauth2Error);
        }
        SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),
                                                        JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));
        return NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
    }
    OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
    throw new OAuth2AuthenticationException(oauth2Error);
}

这里涉及两种 jwt 签名方式:

  • SignatureAlgorithm: 比如 private_jwt 方式
    • 要求jwkSetUrl 不能为空
  • MacAlgorithm: 摘要方式
    • 要求 clientSecret 不能为空

在创建客户端的 jwt 要求有字段必有, 是因为解析jwt的时候增加了校验:

private static OAuth2TokenValidator<Jwt> createJwtValidator(RegisteredClient registeredClient) {
    String clientId = registeredClient.getClientId();
    return new DelegatingOAuth2TokenValidator<>(
        new JwtClaimValidator<>(JwtClaimNames.ISS, clientId::equals),
        new JwtClaimValidator<>(JwtClaimNames.SUB, clientId::equals),
        new JwtClaimValidator<>(JwtClaimNames.AUD, containsProviderAudience()),
        new JwtClaimValidator<>(JwtClaimNames.EXP, Objects::nonNull),
        new JwtTimestampValidator()
    );
}

再看看 jwtDecoder.decode 方法, 委托给 NimbusJwtDecoder

public final class NimbusJwtDecoder implements JwtDecoder {


	@Override
	public Jwt decode(String token) throws JwtException {
		JWT jwt = parse(token);
		if (jwt instanceof PlainJWT) {
			this.logger.trace("Failed to decode unsigned token");
			throw new BadJwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm());
		}
		Jwt createdJwt = createJwt(token, jwt);
		return validateJwt(createdJwt);
	}

	private JWT parse(String token) {
		try {
			return JWTParser.parse(token);
		}
		catch (Exception ex) {
			this.logger.trace("Failed to parse token", ex);
			throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
		}
	}

	private Jwt createJwt(String token, JWT parsedJwt) {
		try {
			// Verify the signature
			JWTClaimsSet jwtClaimsSet = this.jwtProcessor.process(parsedJwt, null);
			Map<String, Object> headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject());
			Map<String, Object> claims = this.claimSetConverter.convert(jwtClaimsSet.getClaims());
			// @formatter:off
			return Jwt.withTokenValue(token)
					.headers((h) -> h.putAll(headers))
					.claims((c) -> c.putAll(claims))
					.build();
			// @formatter:on
		}
		catch (RemoteKeySourceException ex) {
			this.logger.trace("Failed to retrieve JWK set", ex);
			if (ex.getCause() instanceof ParseException) {
				throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed Jwk set"), ex);
			}
			throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
		}
		catch (JOSEException ex) {
			this.logger.trace("Failed to process JWT", ex);
			throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
		}
		catch (Exception ex) {
			this.logger.trace("Failed to process JWT", ex);
			if (ex.getCause() instanceof ParseException) {
				throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed payload"), ex);
			}
			throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
		}
	}

}

很简单:

  • 解析 jwt
  • 根据相同规则生成 jwt

jwtProcessor.process 方法:

//DefaultJWTProcessor.java
@Override
public JWTClaimsSet process(final JWT jwt, final C context)
    throws BadJOSEException, JOSEException {

    if (jwt instanceof SignedJWT) {
        return process((SignedJWT)jwt, context);
    }

    if (jwt instanceof EncryptedJWT) {
        return process((EncryptedJWT)jwt, context);
    }

    if (jwt instanceof PlainJWT) {
        return process((PlainJWT)jwt, context);
    }

    // Should never happen
    throw new JOSEException("Unexpected JWT object type: " + jwt.getClass());
}

如果 jwt 不是摘要签名方式, 就需要提供 jwk 的获取方式, 如 private_key_jwt 方式,就需要配置:

@Bean
public JWKSource<SecurityContext> jwkSource() {
    RSAKey rsaKey = Jwks.generateRsa();
    JWKSet jwkSet = new JWKSet(rsaKey);
    return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

在 JWEDecryptionKeySelector 使用到:

public class JWEDecryptionKeySelector<C extends SecurityContext>
    extends AbstractJWKSelectorWithSource<C> implements JWEKeySelector<C> {


	@Override
	public List<Key> selectJWEKeys(final JWEHeader jweHeader, final C context)
		throws KeySourceException {

		if (! jweAlg.equals(jweHeader.getAlgorithm()) || ! jweEnc.equals(jweHeader.getEncryptionMethod())) {
			// Unexpected JWE alg or enc
			return Collections.emptyList();
		}

		JWKMatcher jwkMatcher = createJWKMatcher(jweHeader);
        // 这里获取 JWKSouce!!
		List<JWK> jwkMatches = getJWKSource().get(new JWKSelector(jwkMatcher), context);
		List<Key> sanitizedKeyList = new LinkedList<>();

		for (Key key: KeyConverter.toJavaKeys(jwkMatches)) {
			if (key instanceof PrivateKey || key instanceof SecretKey) {
				sanitizedKeyList.add(key);
			} // skip public keys
		}

		return sanitizedKeyList;
	}
}

 类似资料: