diff --git a/docs/src/docs/asciidoc/examples/src/main/java/sample/extgrant/CustomCodeGrantAuthenticationConverter.java b/docs/src/docs/asciidoc/examples/src/main/java/sample/extgrant/CustomCodeGrantAuthenticationConverter.java new file mode 100644 index 00000000..5c128c34 --- /dev/null +++ b/docs/src/docs/asciidoc/examples/src/main/java/sample/extgrant/CustomCodeGrantAuthenticationConverter.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.extgrant; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.lang.Nullable; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +public class CustomCodeGrantAuthenticationConverter implements AuthenticationConverter { + + @Nullable + @Override + public Authentication convert(HttpServletRequest request) { + // grant_type (REQUIRED) + String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); + if (!"urn:ietf:params:oauth:grant-type:custom_code".equals(grantType)) { // <1> + return null; + } + + Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); + + MultiValueMap parameters = getParameters(request); + + // code (REQUIRED) + String code = parameters.getFirst(OAuth2ParameterNames.CODE); // <2> + if (!StringUtils.hasText(code) || + parameters.get(OAuth2ParameterNames.CODE).size() != 1) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); + } + + Map additionalParameters = new HashMap<>(); + parameters.forEach((key, value) -> { + if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) && + !key.equals(OAuth2ParameterNames.CLIENT_ID) && + !key.equals(OAuth2ParameterNames.CODE)) { + additionalParameters.put(key, value.get(0)); + } + }); + + return new CustomCodeGrantAuthenticationToken(code, clientPrincipal, additionalParameters); // <3> + } + + // @fold:on + private static MultiValueMap getParameters(HttpServletRequest request) { + Map parameterMap = request.getParameterMap(); + MultiValueMap parameters = new LinkedMultiValueMap<>(parameterMap.size()); + parameterMap.forEach((key, values) -> { + if (values.length > 0) { + for (String value : values) { + parameters.add(key, value); + } + } + }); + return parameters; + } + // @fold:off + +} diff --git a/docs/src/docs/asciidoc/examples/src/main/java/sample/extgrant/CustomCodeGrantAuthenticationProvider.java b/docs/src/docs/asciidoc/examples/src/main/java/sample/extgrant/CustomCodeGrantAuthenticationProvider.java new file mode 100644 index 00000000..b70e07f6 --- /dev/null +++ b/docs/src/docs/asciidoc/examples/src/main/java/sample/extgrant/CustomCodeGrantAuthenticationProvider.java @@ -0,0 +1,129 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.extgrant; + +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.ClaimAccessor; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; +import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.util.Assert; + +public class CustomCodeGrantAuthenticationProvider implements AuthenticationProvider { + // @fold:on + private final OAuth2AuthorizationService authorizationService; + private final OAuth2TokenGenerator tokenGenerator; + + public CustomCodeGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, + OAuth2TokenGenerator tokenGenerator) { + Assert.notNull(authorizationService, "authorizationService cannot be null"); + Assert.notNull(tokenGenerator, "tokenGenerator cannot be null"); + this.authorizationService = authorizationService; + this.tokenGenerator = tokenGenerator; + } + // @fold:off + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + CustomCodeGrantAuthenticationToken customCodeGrantAuthentication = + (CustomCodeGrantAuthenticationToken) authentication; + + // Ensure the client is authenticated + OAuth2ClientAuthenticationToken clientPrincipal = + getAuthenticatedClientElseThrowInvalidClient(customCodeGrantAuthentication); + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + // Ensure the client is configured to use this authorization grant type + if (!registeredClient.getAuthorizationGrantTypes().contains(customCodeGrantAuthentication.getGrantType())) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); + } + + // TODO Validate the code parameter + + // Generate the access token + OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .principal(clientPrincipal) + .authorizationServerContext(AuthorizationServerContextHolder.getContext()) + .tokenType(OAuth2TokenType.ACCESS_TOKEN) + .authorizationGrantType(customCodeGrantAuthentication.getGrantType()) + .authorizationGrant(customCodeGrantAuthentication) + .build(); + + OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext); + if (generatedAccessToken == null) { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, + "The token generator failed to generate the access token.", null); + throw new OAuth2AuthenticationException(error); + } + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), + generatedAccessToken.getExpiresAt(), null); + + // Initialize the OAuth2Authorization + OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) + .principalName(clientPrincipal.getName()) + .authorizationGrantType(customCodeGrantAuthentication.getGrantType()); + if (generatedAccessToken instanceof ClaimAccessor) { + authorizationBuilder.token(accessToken, (metadata) -> + metadata.put( + OAuth2Authorization.Token.CLAIMS_METADATA_NAME, + ((ClaimAccessor) generatedAccessToken).getClaims()) + ); + } else { + authorizationBuilder.accessToken(accessToken); + } + OAuth2Authorization authorization = authorizationBuilder.build(); + + // Save the OAuth2Authorization + this.authorizationService.save(authorization); + + return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken); + } + + @Override + public boolean supports(Class authentication) { + return CustomCodeGrantAuthenticationToken.class.isAssignableFrom(authentication); + } + + // @fold:on + private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) { + OAuth2ClientAuthenticationToken clientPrincipal = null; + if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) { + clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal(); + } + if (clientPrincipal != null && clientPrincipal.isAuthenticated()) { + return clientPrincipal; + } + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } + // @fold:off + +} diff --git a/docs/src/docs/asciidoc/examples/src/main/java/sample/extgrant/CustomCodeGrantAuthenticationToken.java b/docs/src/docs/asciidoc/examples/src/main/java/sample/extgrant/CustomCodeGrantAuthenticationToken.java new file mode 100644 index 00000000..c7067315 --- /dev/null +++ b/docs/src/docs/asciidoc/examples/src/main/java/sample/extgrant/CustomCodeGrantAuthenticationToken.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.extgrant; + +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken; +import org.springframework.util.Assert; + +public class CustomCodeGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { + private final String code; + + public CustomCodeGrantAuthenticationToken(String code, Authentication clientPrincipal, + @Nullable Map additionalParameters) { + super(new AuthorizationGrantType("urn:ietf:params:oauth:grant-type:custom_code"), + clientPrincipal, additionalParameters); + Assert.hasText(code, "code cannot be empty"); + this.code = code; + } + + public String getCode() { + return this.code; + } + +} diff --git a/docs/src/docs/asciidoc/examples/src/main/java/sample/extgrant/SecurityConfig.java b/docs/src/docs/asciidoc/examples/src/main/java/sample/extgrant/SecurityConfig.java new file mode 100644 index 00000000..4effaedb --- /dev/null +++ b/docs/src/docs/asciidoc/examples/src/main/java/sample/extgrant/SecurityConfig.java @@ -0,0 +1,116 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.extgrant; + +import java.util.UUID; + +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator; +import org.springframework.security.oauth2.server.authorization.token.JwtGenerator; +import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator; +import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.RequestMatcher; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + // @formatter:off + @Bean + SecurityFilterChain authorizationServerSecurityFilterChain( + HttpSecurity http, + OAuth2AuthorizationService authorizationService, + OAuth2TokenGenerator tokenGenerator) throws Exception { + + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer(); + + authorizationServerConfigurer + .tokenEndpoint(tokenEndpoint -> + tokenEndpoint + .accessTokenRequestConverter( // <1> + new CustomCodeGrantAuthenticationConverter()) + .authenticationProvider( // <2> + new CustomCodeGrantAuthenticationProvider( + authorizationService, tokenGenerator))); + + // @fold:on + RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); + + http + .securityMatcher(endpointsMatcher) + .authorizeHttpRequests(authorize -> + authorize + .anyRequest().authenticated() + ) + .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) + .apply(authorizationServerConfigurer); + // @fold:off + + return http.build(); + } + // @formatter:on + + // @fold:on + // @formatter:off + @Bean + RegisteredClientRepository registeredClientRepository() { + RegisteredClient messagingClient = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("messaging-client") + .clientSecret("{noop}secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(new AuthorizationGrantType("urn:ietf:params:oauth:grant-type:custom_code")) + .scope("message.read") + .scope("message.write") + .build(); + + return new InMemoryRegisteredClientRepository(messagingClient); + } + // @formatter:on + + @Bean + OAuth2AuthorizationService authorizationService() { + return new InMemoryOAuth2AuthorizationService(); + } + + @Bean + OAuth2TokenGenerator tokenGenerator(JWKSource jwkSource) { + JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource)); + OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator(); + OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); + return new DelegatingOAuth2TokenGenerator( + jwtGenerator, accessTokenGenerator, refreshTokenGenerator); + } + // @fold:off + +} diff --git a/docs/src/docs/asciidoc/examples/src/test/java/sample/extgrant/CustomCodeGrantTests.java b/docs/src/docs/asciidoc/examples/src/test/java/sample/extgrant/CustomCodeGrantTests.java new file mode 100644 index 00000000..721611e2 --- /dev/null +++ b/docs/src/docs/asciidoc/examples/src/test/java/sample/extgrant/CustomCodeGrantTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.extgrant; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import sample.test.SpringTestContext; +import sample.test.SpringTestContextExtension; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.http.HttpHeaders; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringTestContextExtension.class) +public class CustomCodeGrantTests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private RegisteredClientRepository registeredClientRepository; + + @Autowired + private MockMvc mvc; + + @Test + public void requestWhenTokenRequestValidThenTokenResponse() throws Exception { + this.spring.register(AuthorizationServerConfig.class).autowire(); + + RegisteredClient registeredClient = this.registeredClientRepository.findByClientId("messaging-client"); + + HttpHeaders headers = new HttpHeaders(); + headers.setBasicAuth(registeredClient.getClientId(), + registeredClient.getClientSecret().replace("{noop}", "")); + + // @formatter:off + this.mvc.perform(post("/oauth2/token") + .param(OAuth2ParameterNames.GRANT_TYPE, "urn:ietf:params:oauth:grant-type:custom_code") + .param(OAuth2ParameterNames.CODE, "7QR49T1W3") + .headers(headers)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.access_token").isNotEmpty()); + // @formatter:on + } + + @EnableWebSecurity + @EnableAutoConfiguration + @ComponentScan + static class AuthorizationServerConfig { + } + +} diff --git a/docs/src/docs/asciidoc/guides/how-to-ext-grant-type.adoc b/docs/src/docs/asciidoc/guides/how-to-ext-grant-type.adoc new file mode 100644 index 00000000..6ccc1375 --- /dev/null +++ b/docs/src/docs/asciidoc/guides/how-to-ext-grant-type.adoc @@ -0,0 +1,76 @@ +[[how-to-extension-grant-type]] += How-to: Implement an Extension Authorization Grant Type +:index-link: ../how-to.html +:docs-dir: .. +:examples-dir: {docs-dir}/examples + +This guide shows how to extend xref:{docs-dir}/index.adoc#top[Spring Authorization Server] with an https://datatracker.ietf.org/doc/html/rfc6749#section-4.5[extension authorization grant type]. +The purpose of this guide is to demonstrate how to implement an extension authorization grant type and configure it at the xref:{docs-dir}/protocol-endpoints.adoc#oauth2-token-endpoint[OAuth2 Token endpoint]. + +Extending Spring Authorization Server with a new authorization grant type requires implementing an `AuthenticationConverter` and `AuthenticationProvider`, and configuring both components at the xref:{docs-dir}/protocol-endpoints.adoc#oauth2-token-endpoint[OAuth2 Token endpoint]. +In addition to the component implementations, a unique absolute URI needs to be assigned for use with the `grant_type` parameter. + +* <> +* <> +* <> +* <> + +[[implement-authentication-converter]] +== Implement AuthenticationConverter + +Assuming the absolute URI for the `grant_type` parameter is `urn:ietf:params:oauth:grant-type:custom_code` and the `code` parameter represents the authorization grant, the following example shows a sample implementation of the `AuthenticationConverter`: + +.AuthenticationConverter +[source,java] +---- +include::{examples-dir}/src/main/java/sample/extgrant/CustomCodeGrantAuthenticationConverter.java[] +---- + +TIP: Click on the "Expand folded text" icon in the code sample above to display the full example. + +<1> If the `grant_type` parameter is *not* `urn:ietf:params:oauth:grant-type:custom_code`, then return `null`, allowing another `AuthenticationConverter` to process the token request. +<2> The `code` parameter contains the authorization grant. +<3> Return an instance of `CustomCodeGrantAuthenticationToken`, which is processed by <>. + +[[implement-authentication-provider]] +== Implement AuthenticationProvider + +The `AuthenticationProvider` implementation is responsible for validating the authorization grant, and if valid and authorized, issues an access token. + +The following example shows a sample implementation of the `AuthenticationProvider`: + +.AuthenticationProvider +[source,java] +---- +include::{examples-dir}/src/main/java/sample/extgrant/CustomCodeGrantAuthenticationProvider.java[] +---- + +NOTE: `CustomCodeGrantAuthenticationProvider` processes `CustomCodeGrantAuthenticationToken`, which is created by <>. + +[[configure-token-endpoint]] +== Configure OAuth2 Token Endpoint + +The following example shows how to configure the xref:{docs-dir}/protocol-endpoints.adoc#oauth2-token-endpoint[OAuth2 Token endpoint] with the `AuthenticationConverter` and `AuthenticationProvider`: + +.SecurityConfig +[source,java] +---- +include::{examples-dir}/src/main/java/sample/extgrant/SecurityConfig.java[] +---- + +<1> Add the `AuthenticationConverter` to the OAuth2 Token endpoint configuration. +<2> Add the `AuthenticationProvider` to the OAuth2 Token endpoint configuration. + +[[request-access-token]] +== Request the Access Token + +The client can request the access token by making the following (authenticated) request to the OAuth2 Token endpoint: + +[source,shell] +---- +POST /oauth2/token HTTP/1.1 +Authorization: Basic bWVzc2FnaW5nLWNsaWVudDpzZWNyZXQ= +Content-Type: application/x-www-form-urlencoded + +grant_type=urn:ietf:params:oauth:grant-type:custom_code&code=7QR49T1W3 +---- diff --git a/docs/src/docs/asciidoc/how-to.adoc b/docs/src/docs/asciidoc/how-to.adoc index a5d22d16..4f7104a2 100644 --- a/docs/src/docs/asciidoc/how-to.adoc +++ b/docs/src/docs/asciidoc/how-to.adoc @@ -6,5 +6,6 @@ * xref:guides/how-to-pkce.adoc[Authenticate using a Single Page Application with PKCE] * xref:guides/how-to-social-login.adoc[Authenticate using Social Login] +* xref:guides/how-to-ext-grant-type.adoc[Implement an Extension Authorization Grant Type] * xref:guides/how-to-userinfo.adoc[Customize the OpenID Connect 1.0 UserInfo response] * xref:guides/how-to-jpa.adoc[Implement core services with JPA]