preface

This article will introduce how to access the GitHub user information API based on OAuth2 protocol and how to implement a simple authentication server based on the authorization code mode. If you are not familiar with the basic concepts of OAuth2 and the four authorization modes, you can first read ruan Yifong’s blog: A simple explanation of OAuth 2.0, this article is mainly to explain how to use the actual demo. The complete code for the example shown in this article has been uploaded to GitHub.

GitHub third-party login

Lead to

Before access lot API interface, need to visit https://github.com/settings/applications/new, then fill in the following content:

Except for the last item, the Authorization callback URL, which is used to display the website information when the user clicks on a third party to log in, and the last item is the callback address used to receive the temporary Authorization code in exchange for Access Token. The corresponding figure of D and E, below from https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.

+----------+ | Resource | | Owner | | | +----------+ ^ | (B) +----|-----+ Client Identifier +---------------+ | -+----(A)-- & Redirection URI ---->| | | User- | | Authorization | | Agent -+----(B)-- User authenticates --->| Server |  | | | | | -+----(C)-- Authorization Code ---<| | +-|----|---+ +---------------+ | | ^ v (A) (C) | | | | | | ^ v | | +---------+ | | | |>---(D)-- Authorization Code ---------' | | Client | & Redirection URI | | | | | |<---(E)----- Access  Token -------------------' +---------+ (w/ Optional Refresh Token) Note: The lines illustrating steps (A), (B), and (C) are broken into two parts as they pass through the user-agent. Authorization Code FlowCopy the code

After filling in the above information, it will jump to the following interface:

Here you need to save the Client ID and Client secret generated after clicking Generate a new Client secret for future use.

coding

After completing the above preparation steps, the coding work can start. First of all, in order to facilitate subsequent use and modification, Client ID and Client Secret can be configured in the configuration file (to reduce the length, only part of the core code is shown) :

server:
  port: 8080

oauth:
  github:
    # Replace with your own Client ID and Client Secret
    clientId: 8aed0bc8316548e9d26d
    clientSecret: fdd0e7af5052164e459098703005c5db25f857a8
    GitHub user information generated after the token is passed to the front-end processing address
    frontRedirectUrl: http://localhost/redirect

# a HTTP client (https://github.com/LianjiaTech/retrofit-spring-boot-starter) framework configuration
retrofit:
  global-connect-timeout-ms: 20000
  global-read-timeout-ms: 10000
Copy the code

Then write the corresponding entity GithubAuth:

/** * Github authentication information **@author zjw
 * @dateThe 2021-10-23 * /
@Data
@Component
@ConfigurationProperties(prefix = "oauth.github")
public class GithubAuth {

    /** * Client id */
    private String clientId;

    /** * Client key */
    private String clientSecret;

    /** * Redirect address */
    private String frontRedirectUrl;

}
Copy the code

Then write the GitHub authentication interface service class:

/** * Github oauth interface service class **@author zjw
 * @dateThe 2021-10-23 * /
@RetrofitClient(baseUrl = "https://github.com/login/oauth/")
public interface GithubAuthService {

    /** * Make a Github authorization request **@paramClientId clientId *@paramClientSecret Client key *@paramCode Temporary authorization code *@return access_token
     */
    @POST("access_token")
    @Headers("Accept: application/json")
    GithubToken getToken(
        @Query("client_id") String clientId, 
        @Query("client_secret") String clientSecret, 
        @Query("code") String code);

}
Copy the code

And interface service classes to get user information:

/** * Github interface service class **@author zjw
 * @dateThe 2021-10-23 * /
@RetrofitClient(baseUrl = "https://api.github.com")
public interface GithubApiService {

    /** * Get github user information based on access_token **@paramAuthorization Request authentication header *@returnMaking the user * /
    @GET("/user")
    GithubUser getUserInfo(@Header(HttpHeaders.AUTHORIZATION) String authorization);

}
Copy the code

Then the interface that handles the temporary authorization code (where the interface address corresponds to the callback address filled in above) :

/** * oauth2 Authenticate controller **@author zjw
 * @dateThe 2021-10-23 * /
@RestController
@RequestMapping("/oauth")
public class OauthController {

    @Resource
    private GithubAuth githubAuth;

    @Resource
    private GithubApiService githubApiService;

    @Resource
    private GithubAuthService githubAuthService;

    /** * github redirect address **@paramCode Temporary authorization code *@paramThe response response * /
    @GetMapping("/github/redirect")
    public void githubRedirect(String code, HttpServletResponse response) {
        / / get access_token
        String clientId = githubAuth.getClientId();
        String clientSecret = githubAuth.getClientSecret();
        GithubToken githubToken = githubAuthService.getToken(clientId, clientSecret, code);
        // Get github user information
        String authorization = String.join(
            StringUtils.SPACE, githubToken.getTokenType(), githubToken.getAccessToken());
        GithubUser githubUser = githubApiService.getUserInfo(authorization);
        // Generate a local access token
        String token = JwtUtils.sign(githubUser.getUsername(), UserType.GITHUB.getType());
        try {
            response.sendRedirect(githubAuth.getFrontRedirectUrl() + "? token=" + token);
        } catch (IOException e) {
            throw newApiException(REDIRECT_FAILED); }}}Copy the code

The front end only needs to place the corresponding GitHub icon on the login page and set the click event:

githubAuthorize() {
	const env = process.env
	window.location.href = `https://github.com/login/oauth/authorize?
      	client_id=${env.VUE_APP_GITHUB_CLIENT_ID}
      	&redirect_uri=${env.VUE_APP_GITHUB_REDIRECT_URI}`
}
Copy the code

And perform the following redirect processing on the redirection interface:

created() {
	this.setToken(this.$route.query.token)
    this.$router.push('/')}Copy the code

This is the core configuration of the project, and the final result is as follows:

Self-implementation of OAuth2 authentication server

Lead to

In this article, database storage is used to save client information. Therefore, the following SQL script needs to be executed first to create the corresponding table:

-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details`  (
  `client_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `resource_ids` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `client_secret` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `scope` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `authorized_grant_types` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `web_server_redirect_uri` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `authorities` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `access_token_validity` int(11) NULL DEFAULT NULL,
  `refresh_token_validity` int(11) NULL DEFAULT NULL,
  `additional_information` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `autoapprove` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL.PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details` VALUES ('butterfly'.NULL.'$2a$10$KvJWyf4wI.YcpzmbYGw8NOSlauim7dF9b/VSMOomONJf40Bq8F4Me'.'all'.'authorization_code'.'http://localhost:8080/oauth/oauth2/redirect'.NULL.3600.7200.NULL.'false');
Copy the code

coding

The first is the YAML configuration:

server:
  port: 9002

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3306/oauth2? useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: root
Copy the code

Next is the configuration of the authentication server:

/** * Authentication server configuration **@author zjw
 * @dateThe 2021-10-13 * /
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Resource
    private UserDetailsServiceImpl userDetailsService;

    /** * Configure access_token */ of type JWT
    @Bean
    public TokenStore tokenStore(a) {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /** * Set the token signature */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(a) {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
        accessTokenConverter.setSigningKey(BaseConstants.SECRET);
        return accessTokenConverter;
    }

    /** * Configure the authentication data source */
    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource(a) {
        return DataSourceBuilder.create().build();
    }

    /** * Configure the JDBC authentication mode */
    @Bean
    public ClientDetailsService jdbcClientDetails(a) {
        return new JdbcClientDetailsService(dataSource());
    }

    /** * Configure the JDBC authentication mode */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(jdbcClientDetails());
    }

    /** * Configure the information format and content of the certificate */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.tokenStore(tokenStore())
                .accessTokenConverter(jwtAccessTokenConverter())
                 // Obtain refresh_token
                 .userDetailsService(userDetailsService);
    }

    /** * Enable token authentication */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security.allowFormAuthenticationForClients()
                .tokenKeyAccess("isAuthenticated()")
                .checkTokenAccess("isAuthenticated()"); }}Copy the code

Then there is the custom user permission information, where you can set the user and permission information stored in the token:

/** * Set user permission information **@author zjw
 * @dateThe 2021-10-13 * /
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private IUserService userService;

    /** * Sets the authentication information stored in access_token */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.getByUsername(username);
        if (user == null) {
            throw new ApiException(BaseConstants.AUTHENTICATION_FAILED);
        }
        String account = user.getUsername();
        return new org.springframework.security.core.userdetails.User(
            account, user.getPassword(), 
            Collections.singletonList(newSimpleGrantedAuthority(account)) ); }}Copy the code

Then Web security-related configurations:

/** * Web security configuration **@author zjw
 * @dateThe 2021-10-13 * /
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    /** * Password encryption mode */
    @Bean
    public BCryptPasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }

    /** * Custom user permission configuration */
    @Bean
    @Override
    public UserDetailsService userDetailsService(a) {
        return new UserDetailsServiceImpl();
    }

    /** * Sets the permission information stored in the token */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }

    /** * Cross-domain configuration */
    @Bean
    public CorsConfigurationSource corsConfigurationSource(a) {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Collections.singletonList(ALL));
        configuration.setAllowedMethods(Arrays.asList(
            HttpMethod.POST.name(), HttpMethod.GET.name(), HttpMethod.OPTIONS.name()));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration(ALL_PATTERN, configuration);
        returnsource; }}Copy the code

The above is all the configuration of the authorization server. The following shows the configuration of the resource server. Here, the authorization server and the resource server are still configured separately, and the old configuration method is still used:

/** * Resource server **@author zjw
 * @dateThe 2021-11-21 * /
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .exceptionHandling()
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/user/**").access("#oauth2.hasScope('all')"); }}Copy the code

Then create an interface that returns user information:

/** * Resource controller **@author zjw
 * @dateThe 2021-11-20 * /
@RestController
public class ResourceController {

    /** * Obtain the user name based on the token **@paramAuthorization Token Request header *@returnUser name * /
    @GetMapping("/user/info")
    public String info(@RequestHeader(HttpHeaders.AUTHORIZATION) String authorization) {
        String token = authorization.substring(TokenConstants.BEARER_PREFIX.length());
        returnJWT.decode(token).getClaim(TokenConstants.USERNAME).asString(); }}Copy the code

The yamL configuration is as follows, where http://localhost:9002 needs to be changed to the address of the authorization server:

server:
  port: 9003

security:
  oauth2:
    client:
      client-id: butterfly
      client-secret: 123456
      access-token-uri: http://localhost:9002/oauth/token
      user-authorization-uri: http://localhost:9002/oauth/authorize
    resource:
      token-info-uri: http://localhost:9002/oauth/check_token
Copy the code

After completing the above configuration, you can follow the steps in GitHub to write the client back-end code:

The first is the YAML configuration:

server:
  port: 8080

oauth:
  oauth2:
    clientId: butterfly
    clientSecret: 123456
    frontRedirectUrl: http://localhost/redirect

retrofit:
  global-connect-timeout-ms: 20000
  global-read-timeout-ms: 10000
Copy the code

Then create the corresponding entity:

/** * oauth2 Authentication information **@author zjw
 * @dateThe 2021-10-31 * /
@Data
@Component
@ConfigurationProperties(prefix = "oauth.oauth2")
public class Oauth2Auth {

    /** * Client id */
    private String clientId;

    /** * Client key */
    private String clientSecret;

    /** * Redirect address */
    private String frontRedirectUrl;

}
Copy the code

Then write the corresponding authentication and access to user information interface service class:

/** * oauth2 interface service class **@author zjw
 * @dateThe 2021-10-31 * /
@RetrofitClient(baseUrl = "http://localhost:9002/oauth/")
public interface Oauth2AuthService {

    /** * Make oauth2 authorization request **@paramAuthorization header *@paramCode Temporary authorization code *@paramGrantType Authentication type *@return access_token
     */
    @POST("token")
    Oauth2Token getToken(
        @Header(HttpHeaders.AUTHORIZATION) String authorization, 
        @Query("code") String code,
        @Query("grant_type") String grantType);

}
Copy the code
/** * oauth2 interface service class **@author zjw
 * @dateThe 2021-10-31 * /
@RetrofitClient(baseUrl = "http://localhost:9003")
public interface Oauth2ApiService {

    /** * Get oauth2 user name ** based on access_token@paramAuthorization Request authentication header *@returnOauth2 User name */
    @GET("/user/info")
    String getUserInfo(@Header(HttpHeaders.AUTHORIZATION) String authorization);

}
Copy the code

Then write a callback interface that handles temporary authorization code:

/** * oauth2 Authenticate controller **@author zjw
 * @dateThe 2021-10-23 * /
@RestController
@RequestMapping("/oauth")
public class OauthController {

    @Resource
    private Oauth2Auth oauth2Auth;
    
    @Resource
    private Oauth2AuthService oauth2AuthService;

    @Resource
    private Oauth2ApiService oauth2ApiService;

    /** * oauth2 redirects address **@paramCode Temporary authorization code *@paramThe response response * /
    @GetMapping("/oauth2/redirect")
    public void oauth2Redirect(String code, HttpServletResponse response) {
        / / get access_token
        String clientId = oauth2Auth.getClientId();
        String clientSecret = oauth2Auth.getClientSecret();
        String authorization = BaseConstants.BASIC_TYPE + Base64.getEncoder().encodeToString(
                (String.join(":", clientId, clientSecret)).getBytes()
        );
        // Get the oauth2 user name
        Oauth2Token oauth2Token = oauth2AuthService.getToken(
            authorization, code, "authorization_code");
        String username = oauth2ApiService.getUserInfo(
                String.join(
                    StringUtils.SPACE, 
                    oauth2Token.getTokenType(), 
                    oauth2Token.getAccessToken()
                )
        );
        // Generate a local access token
        String token = JwtUtils.sign(username, UserType.OAUTH2.getType());
        try {
            response.sendRedirect(oauth2Auth.getFrontRedirectUrl() + "? token=" + token);
        } catch (IOException e) {
            throw newApiException(REDIRECT_FAILED); }}}Copy the code

Then add the corresponding icon click event to the front as well:

oauth2Authorize() {
	const env = process.env
	window.location.href = `http://localhost:9002/oauth/authorize?
      	client_id=${env.VUE_APP_OAUTH_CLIENT_ID}&response_type=code`
}
Copy the code

The redirect processing for the redirection interface remains unchanged:

created() {
	this.setToken(this.$route.query.token)
    this.$router.push('/')}Copy the code

The final effect is as follows:

conclusion

This article briefly explains how to access GitHub’s third-party login interface and implement a simple authentication server. In future articles, we will explain how to customize the login and confirmation interface of the authentication server and add custom authentication methods.