[JAVA] I implemented an OAuth client with Spring Boot / Security (LINE login)

1. Authorization server setting (LINE)

LINE Developers: https://developers.line.biz/ja

・ Channel Type: LINE Login ・ App Types: Web app -Callback URL: http : // localhost: 8080 / redirect (URL to redirect after authentication)

2. Client implementation (Spring Boot / Security)

2-1. Addition of dependent libraries

pom.xml


    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
        <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    </dependency>
2-2. Setting application.yml

Based on the following contents, transition the user to the LINE authentication screen and call the LINE API to acquire user information

security.oauth2.client.clientId  - Channnel ID security.oauth2.client.clientSecret  - Channnel Secret security.oauth2.client.accessTokenUri --Endpoint to be called when issuing an access token security.oauth2.client.userAuthorizationUri --Authentication destination endpoint security.oauth2.client.pre-established-redirect-uri --Redirect URL after authentication (* Callback URL set on LINE side) security.oauth2.client.scope --List of scopes that clients can use when requesting tokens security.oauth2.client.clientAuthenticationScheme --Format used for OAuth2 Token Endpoint call (* LINE supports FORM) security.oauth2.resource.userInfoUri --Endpoint provided for obtaining user information (user ID, user name, profile image)

application.yml


spring:
  main:
    allow-bean-definition-overriding: true
security:
  oauth2:
    client:
      clientId: [CLIENT_ID]
      clientSecret: [CLIENT_SECRET]
      accessTokenUri: https://api.line.me/oauth2/v2.1/token
      userAuthorizationUri: https://access.line.me/oauth2/v2.1/authorize
      use-current-uri: false
      pre-established-redirect-uri: https://localhost:8443/redirect
      authorized-grant-types:
      - authorization_code
      - refresh_token
      clientAuthenticationScheme: form
      scope:
        - openid
        - email
        - profile
    resource:
      userInfoUri: https://api.line.me/oauth2/v2.1/userinfo
      preferTokenInfo: true
server:
  port: 8443
  ssl:
    key-store: [.p12 file path]
    key-store-password: [password]
    keyStoreType: PKCS12
    keyAlias: MyAlias
logging:
  level:
     org.springframework.web: DEBUG    
2-3. Get variable information from application.yml

ConfigComponent.java


import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@PropertySource("classpath:application.yml")
@ConfigurationProperties(prefix = "security.oauth2.client")
public class ConfigComponent {

    private String accessTokenUri;

    private String clientId;

    private String clientSecret;
    
    @Value("${security.oauth2.client.pre-established-redirect-uri}")
    private String redirecturi;
    
    @Value("${security.oauth2.resource.userInfoUri}")
    private String userInfoUri;

    public String getAccessTokenUri() {
        return this.accessTokenUri;
    }
    
    public void setAccessTokenUri(String accessTokenUri) {
        this.accessTokenUri = accessTokenUri;
    }

    public String getClientId() {
        return this.clientId;
    }
    
    public void setClientId(String clientId) {
        this.clientId = clientId;
    }

    public String getRedirecturi() {
        return this.redirecturi;
    }

    public void setRedirecturi(String redirecturi) {
        this.redirecturi = redirecturi;
    }
    
    public String getClientSecret() {
        return this.clientSecret;
    }
    public void setClientSecret(String clientSecret) {
        this.clientSecret = clientSecret;
    }
    
    public String getUserInfoUri() {
        return this.userInfoUri;
    }
    public void setUserInfoUri(String userInfoUri) {
        this.userInfoUri = userInfoUri;
    }
}
2-4. @Bean registration

--Register the variable information acquisition class set above in the bean

ConfigBean.java


package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfig {

    @Bean
    public ConfigComponent configComponent() {
        return new ConfigComponent();
    }
    
    @Bean
    public LoginRestClient loginRestClient() {
        return new LoginRestClient();
    }
}
2-5. Execute OAuth authentication when the user accesses the specified path (/ login)

SecurityConfig.java


import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;

@EnableWebSecurity
@EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                            "/images/**",
                            "/css/**",
                            "/javascript/**",
                            "/webjars/**",
                            "/favicon.ico");
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	http
	    	.authorizeRequests()
		        .antMatchers("/redirect/**")
		        	.permitAll()
		        .and()
	        .authorizeRequests()
		        .antMatchers("/login/**")
		        .authenticated();
    }   
}
2-5. Definition of @Controller

--After authentication is completed, obtain authorization code and execute access token issuance / user information acquisition request -Inject the LoginRestClient class with @Autowired --Pass the obtained object to Thymeleaf template with ModelAndView class

LoginController.java


import static java.util.Objects.requireNonNull;

import java.io.IOException;
import java.util.stream.IntStream;

import com.example.demo.LoginRestClient;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class LoginController {
	
	@Autowired
	LoginRestClient loginRestClient;
	
	@RequestMapping(value = "/redirect", method = RequestMethod.GET)
	public String getToken(@RequestParam("code") String code,
			HttpServletRequest req, HttpServletResponse res) throws IOException{
	  LineToken responseToken = loginRestClient.getToken(code);
	  String accessTK = responseToken.getAccessTK();
	  String refreshTK = responseToken.getRefreshTK();
	  String idTK = responseToken.getIdTK();
	  
	  Cookie accessTKCookie = new Cookie("access_token", accessTK);
	  accessTKCookie.setSecure(true);
	  Cookie refreshTKCookie = new Cookie("refresh_token", refreshTK);
	  refreshTKCookie.setSecure(true);
	  Cookie idTKCookie = new Cookie("id_token", idTK);
	  idTKCookie.setSecure(true);
	  
	  res.addCookie(accessTKCookie);
	  res.addCookie(refreshTKCookie);
	  res.addCookie(idTKCookie);
	  
	  return "redirect:/";
    }

    @RequestMapping(value = "/userinfo", method = RequestMethod.GET)
    public ModelAndView getSubInfo(ModelAndView mv, HttpServletRequest req) throws IOException {
    	Cookie cookies[] = req.getCookies();
    	for(Cookie c: cookies) {
    		if(c.getName().equals("access_token")) {
    			System.err.println(c.getValue());
    			JSONObject userInfo =
    					new JSONObject(loginRestClient.getUserInfo(c.getValue()).getBody());

    		  	String sub = userInfo.getString("sub");
    		  	String name = userInfo.getString("name");
    		  	String picture = userInfo.getString("picture");

    			mv.addObject("sub", sub);
    			mv.addObject("name", name);
    			mv.addObject("picture", picture);
    			mv.setViewName("userinfo");
    		}
    	}
    	return mv;
    	
    	}
    
	@RequestMapping(value = "/", method = RequestMethod.GET)
 	public ModelAndView getTopPage(ModelAndView mv, HttpServletRequest req) {
    	Cookie cookies[] = req.getCookies();
    	for(Cookie c: cookies) {
    		if(c.getName().equals("access_token")) {
    			mv.addObject("access_token", c.getValue());
    		} else if (c.getName().equals("refresh_token")) {
    			mv.addObject("refresh_token", c.getValue());
    		} else if (c.getName().equals("id_token")) {
    			mv.addObject("id_token", c.getValue());
    		}
    	}

    	mv.setViewName("index");
    	
    	return mv;
     }
}
2-6. Request execution class for access token issuance / user information acquisition

--Request access token issuing API with authorization code as argument with getToken method --Get user information by giving an access token to the Authorization header with the getUserInfo method.

LoginRestClient.java


import java.io.IOException;
import java.util.Map;
import java.util.Objects;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

@Component
public class LoginRestClient {
	
	final static RestTemplate rs = new RestTemplate();

	@Autowired
	private ConfigComponent cc;
	
    public LineToken getToken(String code) throws IOException {
    	code = Objects.requireNonNull(code);
    	HttpHeaders headers = new HttpHeaders();
    	headers.setBasicAuth(cc.getClientId(), cc.getClientSecret());
    	headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    	
    	MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
    	params.add("code", code);
    	params.add("redirect_uri", cc.getRedirecturi());
    	params.add("grant_type", "authorization_code");
    	
    	HttpEntity<MultiValueMap<String, Object>> requestEntity 
    		= new HttpEntity<>(params, headers);
    	
        LineToken response = rs
                .postForObject(cc.getAccessTokenUri(), requestEntity, LineToken.class);
    	
    	return response;
    }
    
    public ResponseEntity<String> getUserInfo(String accessToken) throws IOException {
    	accessToken = Objects.requireNonNull(accessToken);
    	HttpHeaders headers = new HttpHeaders();
    	headers.setBearerAuth(accessToken);
    	
    	HttpEntity<MultiValueMap<String, Object>> requestEntity 
			= new HttpEntity<>(headers);
    	
    	 ResponseEntity<String> response =
    			 rs.exchange(cc.getUserInfoUri(), HttpMethod.GET, requestEntity, String.class);
    	
    	System.err.println(response);
    	
    	return response;
    }

}
2-7. Convert access token / refresh token / ID token to POJO

LineToken.java


import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

import static java.util.Objects.requireNonNull;

public final class LineToken {
    public final String accessToken;
    public final String idToken;
    public final String refreshToken;

    @JsonCreator
    LineToken(
        @JsonProperty("access_token") String accessToken,
        @JsonProperty("id_token") String idToken,
        @JsonProperty("refresh_token") String refreshToken
    ) {
        this.accessToken = requireNonNull(accessToken, "accessToken");
        this.idToken = requireNonNull(idToken, "idToken");
        this.refreshToken = requireNonNull(refreshToken, "refreshToken");
    }
    
    public String getAccessTK() {return this.accessToken;}
    public String getIdTK() {return this.idToken;}
    public String getrefreshTK() {return this.refreshToken;}
    
}

3. Verification

3-1. Access https : // localhost: 8443 / login
3-2. Redirect to LINE login screen
キャプチャ.PNG
3-3. Login execution

--With the refresh token, the access token can be reissued by requesting the token even after the expiration date of the access token (30 days later). --The refresh token is valid for up to 10 days after the access token expires. キャプチャ.PNG キャプチャ.PNG

3-4. Decode id_token

Since the ID Token is returned in JWT format, Base64 decode the header / payload part and refer to the contents.

header: {"typ":"JWT","alg":"HS256"}

payload: {"iss":"https://access.line.me","sub":"**********","aud":"1653797412","exp":1579957272,"iat":1579953672,"amr":["linesso"],"name":"Yu","picture":"https://profile.line-scdn.net/0ho4KGrFFFMBt2PhvCq9pPTEp7PnYBEDZTDg98LVpsanhTXn4aTFx5Lwdpbn9SWnEYHwgtfFs3Znhd"}

Claim name Claim content
iss Token issuer(ISSuer)
sub Token target(SUBubect)
aud Token recipient(AUDience)
exp Token expiration date(EXPiration time)
iat Token issuance time stamp(Issued-AT)

Recommended Posts

I implemented an OAuth client with Spring Boot / Security (LINE login)
Part 1: Try using OAuth 2.0 Login supported by Spring Security 5 with Spring Boot
SNS login with Spring Boot
Login function with Spring Security
Implemented authentication function with Spring Security ②
Implemented authentication function with Spring Security ③
Create an app with Spring Boot 2
Implemented authentication function with Spring Security ①
Create an app with Spring Boot
I tried GraphQL with Spring Boot
[Java] LINE integration with Spring Boot
I tried Flyway with Spring Boot
I tried Lazy Initialization with Spring Boot 2.2.0
Achieve BASIC authentication with Spring Boot + Spring Security
[LINE BOT] I made a ramen BOT with Java (Maven) + Heroku + Spring Boot (1)
Hash passwords with Spring Boot + Spring Security (with salt, with stretching)
Try LDAP authentication with Spring Security (Spring Boot) + OpenLDAP
Try to implement login function with Spring Boot
[Java / Spring Boot] Spring security ④ --Implementation of login process
I wanted to gradle spring boot with multi-project
[Introduction to Spring Boot] Authentication function with Spring Security
Create Spring Cloud Config Server with security with Spring Boot 2.0
I created an api domain with Spring Framework. Part 2
Spring Security usage memo: Cooperation with Spring MVC and Boot
Spring Boot with Spring Security Filter settings and addictive points
Implement a simple Rest API with Spring Security with Spring Boot 2.0
SSO with GitHub OAuth in Spring Boot 1.5.x environment
Create a simple demo site with Spring Security with Spring Boot 2.1
I created an api domain with Spring Framework. Part 1
I wrote a test with Spring Boot + JUnit 5 now
Download with Spring Boot
With Spring boot, password is hashed and member registration & Spring security is used to implement login function.
Spring Boot Introductory Guide I tried [Accessing Data with JPA]
I tried to get started with Swagger using Spring Boot
Implement a simple Rest API with Spring Security & JWT with Spring Boot 2.0
Login with HttpServletRequest # login in Spring Security of Servlet 3.x environment
Generate barcode with Spring Boot
Hello World with Spring Boot
Implement GraphQL with Spring Boot
Get started with Spring boot
Hello World with Spring Boot!
Run LIFF with Spring Boot
File upload with Spring Boot
Spring Boot starting with copy
Spring Boot starting with Docker
Hello World with Spring Boot
Set cookies with Spring Boot
Use Spring JDBC with Spring Boot
Add module with Spring Boot
Getting Started with Spring Boot
[Spring Boot] Send an email
Try using Spring Boot Security
Create microservices with Spring Boot
Send email with spring boot
API integration from Java with Jersey Client
Java to play with Function
GraphQL Client starting with Ruby
I implemented an OAuth client with Spring Boot / Security (LINE login)
[Java EE] Implement Client with WebSocket
Double submit measures with Play Framework
Oauth2 authentication with Spring Cloud Gateway
Play with Markdown in Java flexmark-java
Image processing: Let's play with the image
I get a 404 error when testing forms authentication with Spring Security
02. I made an API to connect to MySQL (MyBatis) from Spring Boot
Part 4: Customize the behavior of OAuth 2.0 Login supported by Spring Security 5
I made a simple search form with Spring Boot + GitHub Search API.
Spring Boot + Java + GitHub authentication login
Database linkage with doma2 (Spring boot)