Commit 7fc46010b790f0453a14a70ffba77383351e8734
Committed by
Andrew Shvayka
1 parent
08ab9752
Added base impl for OAuth-2
Showing
9 changed files
with
295 additions
and
2 deletions
@@ -133,6 +133,18 @@ | @@ -133,6 +133,18 @@ | ||
133 | <artifactId>spring-boot-starter-websocket</artifactId> | 133 | <artifactId>spring-boot-starter-websocket</artifactId> |
134 | </dependency> | 134 | </dependency> |
135 | <dependency> | 135 | <dependency> |
136 | + <groupId>org.springframework.cloud</groupId> | ||
137 | + <artifactId>spring-cloud-starter-oauth2</artifactId> | ||
138 | + </dependency> | ||
139 | + <dependency> | ||
140 | + <groupId>org.springframework.security</groupId> | ||
141 | + <artifactId>spring-security-oauth2-client</artifactId> | ||
142 | + </dependency> | ||
143 | + <dependency> | ||
144 | + <groupId>org.springframework.security</groupId> | ||
145 | + <artifactId>spring-security-oauth2-jose</artifactId> | ||
146 | + </dependency> | ||
147 | + <dependency> | ||
136 | <groupId>io.jsonwebtoken</groupId> | 148 | <groupId>io.jsonwebtoken</groupId> |
137 | <artifactId>jjwt</artifactId> | 149 | <artifactId>jjwt</artifactId> |
138 | </dependency> | 150 | </dependency> |
application/src/main/java/org/thingsboard/server/config/ThingsboardOAuth2Configuration.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2020 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.config; | ||
17 | + | ||
18 | +import org.springframework.beans.factory.annotation.Value; | ||
19 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
20 | +import org.springframework.context.annotation.Bean; | ||
21 | +import org.springframework.context.annotation.Configuration; | ||
22 | +import org.springframework.security.oauth2.client.registration.ClientRegistration; | ||
23 | +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; | ||
24 | +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; | ||
25 | +import org.springframework.security.oauth2.core.AuthorizationGrantType; | ||
26 | +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; | ||
27 | + | ||
28 | +import java.util.Collections; | ||
29 | + | ||
30 | +@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true") | ||
31 | +@Configuration | ||
32 | +public class ThingsboardOAuth2Configuration { | ||
33 | + | ||
34 | + @Value("${security.oauth2.registrationId}") | ||
35 | + private String registrationId; | ||
36 | + @Value("${security.oauth2.userNameAttributeName}") | ||
37 | + private String userNameAttributeName; | ||
38 | + | ||
39 | + @Value("${security.oauth2.client.clientId}") | ||
40 | + private String clientId; | ||
41 | + @Value("${security.oauth2.client.clientName}") | ||
42 | + private String clientName; | ||
43 | + @Value("${security.oauth2.client.clientSecret}") | ||
44 | + private String clientSecret; | ||
45 | + @Value("${security.oauth2.client.accessTokenUri}") | ||
46 | + private String accessTokenUri; | ||
47 | + @Value("${security.oauth2.client.authorizationUri}") | ||
48 | + private String authorizationUri; | ||
49 | + @Value("${security.oauth2.client.redirectUriTemplate}") | ||
50 | + private String redirectUriTemplate; | ||
51 | + @Value("${security.oauth2.client.scope}") | ||
52 | + private String scope; | ||
53 | + @Value("${security.oauth2.client.jwkSetUri}") | ||
54 | + private String jwkSetUri; | ||
55 | + @Value("${security.oauth2.client.authorizationGrantType}") | ||
56 | + private String authorizationGrantType; | ||
57 | + @Value("${security.oauth2.client.clientAuthenticationMethod}") | ||
58 | + private String clientAuthenticationMethod; | ||
59 | + | ||
60 | + @Value("${security.oauth2.resource.userInfoUri}") | ||
61 | + private String userInfoUri; | ||
62 | + | ||
63 | + @Bean | ||
64 | + public ClientRegistrationRepository clientRegistrationRepository() { | ||
65 | + ClientRegistration registration = ClientRegistration.withRegistrationId(registrationId) | ||
66 | + .clientId(clientId) | ||
67 | + .authorizationUri(authorizationUri) | ||
68 | + .clientSecret(clientSecret) | ||
69 | + .tokenUri(accessTokenUri) | ||
70 | + .redirectUriTemplate(redirectUriTemplate) | ||
71 | + .scope(scope.split(",")) | ||
72 | + .clientName(clientName) | ||
73 | + .authorizationGrantType(new AuthorizationGrantType(authorizationGrantType)) | ||
74 | + .userInfoUri(userInfoUri) | ||
75 | + .userNameAttributeName(userNameAttributeName) | ||
76 | + .jwkSetUri(jwkSetUri) | ||
77 | + .clientAuthenticationMethod(new ClientAuthenticationMethod(clientAuthenticationMethod)) | ||
78 | + .build(); | ||
79 | + return new InMemoryClientRegistrationRepository(Collections.singletonList(registration)); | ||
80 | + } | ||
81 | +} |
@@ -18,6 +18,8 @@ package org.thingsboard.server.config; | @@ -18,6 +18,8 @@ package org.thingsboard.server.config; | ||
18 | import com.fasterxml.jackson.databind.ObjectMapper; | 18 | import com.fasterxml.jackson.databind.ObjectMapper; |
19 | import org.springframework.beans.factory.annotation.Autowired; | 19 | import org.springframework.beans.factory.annotation.Autowired; |
20 | import org.springframework.beans.factory.annotation.Qualifier; | 20 | import org.springframework.beans.factory.annotation.Qualifier; |
21 | +import org.springframework.beans.factory.annotation.Required; | ||
22 | +import org.springframework.beans.factory.annotation.Value; | ||
21 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; | 23 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
22 | import org.springframework.boot.autoconfigure.security.SecurityProperties; | 24 | import org.springframework.boot.autoconfigure.security.SecurityProperties; |
23 | import org.springframework.context.annotation.Bean; | 25 | import org.springframework.context.annotation.Bean; |
@@ -73,12 +75,25 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt | @@ -73,12 +75,25 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt | ||
73 | public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**"; | 75 | public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**"; |
74 | 76 | ||
75 | @Autowired private ThingsboardErrorResponseHandler restAccessDeniedHandler; | 77 | @Autowired private ThingsboardErrorResponseHandler restAccessDeniedHandler; |
76 | - @Autowired private AuthenticationSuccessHandler successHandler; | 78 | + |
79 | + @Autowired(required = false) | ||
80 | + @Qualifier("oauth2AuthenticationSuccessHandler") | ||
81 | + private AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler; | ||
82 | + | ||
83 | + @Autowired | ||
84 | + @Qualifier("defaultAuthenticationSuccessHandler") | ||
85 | + private AuthenticationSuccessHandler successHandler; | ||
86 | + | ||
77 | @Autowired private AuthenticationFailureHandler failureHandler; | 87 | @Autowired private AuthenticationFailureHandler failureHandler; |
78 | @Autowired private RestAuthenticationProvider restAuthenticationProvider; | 88 | @Autowired private RestAuthenticationProvider restAuthenticationProvider; |
79 | @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider; | 89 | @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider; |
80 | @Autowired private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider; | 90 | @Autowired private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider; |
81 | 91 | ||
92 | + @Value("${security.oauth2.enabled}") | ||
93 | + private boolean oauth2Enabled; | ||
94 | + @Value("${security.oauth2.client.loginProcessingUrl}") | ||
95 | + private String loginProcessingUrl; | ||
96 | + | ||
82 | @Autowired | 97 | @Autowired |
83 | @Qualifier("jwtHeaderTokenExtractor") | 98 | @Qualifier("jwtHeaderTokenExtractor") |
84 | private TokenExtractor jwtHeaderTokenExtractor; | 99 | private TokenExtractor jwtHeaderTokenExtractor; |
@@ -189,6 +204,11 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt | @@ -189,6 +204,11 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt | ||
189 | .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) | 204 | .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) |
190 | .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) | 205 | .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) |
191 | .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); | 206 | .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); |
207 | + if (oauth2Enabled) { | ||
208 | + http.oauth2Login() | ||
209 | + .loginProcessingUrl(loginProcessingUrl) | ||
210 | + .successHandler(oauth2AuthenticationSuccessHandler); | ||
211 | + } | ||
192 | } | 212 | } |
193 | 213 | ||
194 | 214 |
1 | +/** | ||
2 | + * Copyright © 2016-2020 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.service.security.auth.oauth; | ||
17 | + | ||
18 | +import com.fasterxml.jackson.databind.ObjectMapper; | ||
19 | +import org.springframework.beans.factory.annotation.Autowired; | ||
20 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
21 | +import org.springframework.security.authentication.InsufficientAuthenticationException; | ||
22 | +import org.springframework.security.authentication.LockedException; | ||
23 | +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
24 | +import org.springframework.security.core.Authentication; | ||
25 | +import org.springframework.security.core.userdetails.UsernameNotFoundException; | ||
26 | +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; | ||
27 | +import org.springframework.stereotype.Component; | ||
28 | +import org.thingsboard.server.common.data.User; | ||
29 | +import org.thingsboard.server.common.data.id.TenantId; | ||
30 | +import org.thingsboard.server.common.data.security.UserCredentials; | ||
31 | +import org.thingsboard.server.dao.user.UserService; | ||
32 | +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; | ||
33 | +import org.thingsboard.server.service.security.model.SecurityUser; | ||
34 | +import org.thingsboard.server.service.security.model.UserPrincipal; | ||
35 | +import org.thingsboard.server.service.security.model.token.JwtToken; | ||
36 | +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; | ||
37 | +import org.thingsboard.server.service.security.system.SystemSecurityService; | ||
38 | + | ||
39 | +import javax.servlet.ServletException; | ||
40 | +import javax.servlet.http.HttpServletRequest; | ||
41 | +import javax.servlet.http.HttpServletResponse; | ||
42 | +import java.io.IOException; | ||
43 | +import java.util.HashMap; | ||
44 | +import java.util.Map; | ||
45 | + | ||
46 | +@Component(value="oauth2AuthenticationSuccessHandler") | ||
47 | +@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true") | ||
48 | +public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { | ||
49 | + | ||
50 | + private final ObjectMapper mapper; | ||
51 | + private final JwtTokenFactory tokenFactory; | ||
52 | + private final RefreshTokenRepository refreshTokenRepository; | ||
53 | + private final SystemSecurityService systemSecurityService; | ||
54 | + private final UserService userService; | ||
55 | + | ||
56 | + @Autowired | ||
57 | + public Oauth2AuthenticationSuccessHandler(final ObjectMapper mapper, | ||
58 | + final JwtTokenFactory tokenFactory, | ||
59 | + final RefreshTokenRepository refreshTokenRepository, | ||
60 | + final UserService userService, | ||
61 | + final SystemSecurityService systemSecurityService) { | ||
62 | + this.mapper = mapper; | ||
63 | + this.tokenFactory = tokenFactory; | ||
64 | + this.refreshTokenRepository = refreshTokenRepository; | ||
65 | + this.userService = userService; | ||
66 | + this.systemSecurityService = systemSecurityService; | ||
67 | + } | ||
68 | + | ||
69 | + @Override | ||
70 | + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { | ||
71 | + Object object = authentication.getPrincipal(); | ||
72 | + | ||
73 | + System.out.println(object); | ||
74 | + | ||
75 | + // active user check | ||
76 | + | ||
77 | + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, "tenant@thingsboard.org"); | ||
78 | + SecurityUser securityUser = (SecurityUser) authenticateByUsernameAndPassword(principal,"tenant@thingsboard.org", "tenant").getPrincipal(); | ||
79 | + | ||
80 | + JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); | ||
81 | + JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); | ||
82 | + | ||
83 | + Map<String, String> tokenMap = new HashMap<String, String>(); | ||
84 | + tokenMap.put("token", accessToken.getToken()); | ||
85 | + tokenMap.put("refreshToken", refreshToken.getToken()); | ||
86 | + | ||
87 | +// response.setStatus(HttpStatus.OK.value()); | ||
88 | +// response.setContentType(MediaType.APPLICATION_JSON_VALUE); | ||
89 | +// mapper.writeValue(response.getWriter(), tokenMap); | ||
90 | + | ||
91 | + request.setAttribute("token", accessToken.getToken()); | ||
92 | + response.addHeader("token", accessToken.getToken()); | ||
93 | + | ||
94 | + getRedirectStrategy().sendRedirect(request, response, "http://localhost:4200/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken()); | ||
95 | + } | ||
96 | + | ||
97 | + private Authentication authenticateByUsernameAndPassword(UserPrincipal userPrincipal, String username, String password) { | ||
98 | + User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, username); | ||
99 | + if (user == null) { | ||
100 | + throw new UsernameNotFoundException("User not found: " + username); | ||
101 | + } | ||
102 | + | ||
103 | + try { | ||
104 | + | ||
105 | + UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId()); | ||
106 | + if (userCredentials == null) { | ||
107 | + throw new UsernameNotFoundException("User credentials not found"); | ||
108 | + } | ||
109 | + | ||
110 | + try { | ||
111 | + systemSecurityService.validateUserCredentials(user.getTenantId(), userCredentials, username, password); | ||
112 | + } catch (LockedException e) { | ||
113 | + throw e; | ||
114 | + } | ||
115 | + | ||
116 | + if (user.getAuthority() == null) | ||
117 | + throw new InsufficientAuthenticationException("User has no authority assigned"); | ||
118 | + | ||
119 | + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); | ||
120 | + return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); | ||
121 | + } catch (Exception e) { | ||
122 | + throw e; | ||
123 | + } | ||
124 | + } | ||
125 | +} |
@@ -36,7 +36,7 @@ import java.io.IOException; | @@ -36,7 +36,7 @@ import java.io.IOException; | ||
36 | import java.util.HashMap; | 36 | import java.util.HashMap; |
37 | import java.util.Map; | 37 | import java.util.Map; |
38 | 38 | ||
39 | -@Component | 39 | +@Component(value="defaultAuthenticationSuccessHandler") |
40 | public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { | 40 | public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { |
41 | private final ObjectMapper mapper; | 41 | private final ObjectMapper mapper; |
42 | private final JwtTokenFactory tokenFactory; | 42 | private final JwtTokenFactory tokenFactory; |
@@ -26,6 +26,7 @@ | @@ -26,6 +26,7 @@ | ||
26 | </appender> | 26 | </appender> |
27 | 27 | ||
28 | <logger name="org.thingsboard.server" level="INFO" /> | 28 | <logger name="org.thingsboard.server" level="INFO" /> |
29 | + <logger name="org.springframework.security" level="DEBUG" /> | ||
29 | <logger name="akka" level="INFO" /> | 30 | <logger name="akka" level="INFO" /> |
30 | 31 | ||
31 | <!-- <logger name="org.thingsboard.server.service.queue" level="TRACE" />--> | 32 | <!-- <logger name="org.thingsboard.server.service.queue" level="TRACE" />--> |
@@ -97,6 +97,44 @@ security: | @@ -97,6 +97,44 @@ security: | ||
97 | allowClaimingByDefault: "${SECURITY_CLAIM_ALLOW_CLAIMING_BY_DEFAULT:true}" | 97 | allowClaimingByDefault: "${SECURITY_CLAIM_ALLOW_CLAIMING_BY_DEFAULT:true}" |
98 | # Time allowed to claim the device in milliseconds | 98 | # Time allowed to claim the device in milliseconds |
99 | duration: "${SECURITY_CLAIM_DURATION:60000}" # 1 minute, note this value must equal claimDevices.timeToLiveInMinutes value | 99 | duration: "${SECURITY_CLAIM_DURATION:60000}" # 1 minute, note this value must equal claimDevices.timeToLiveInMinutes value |
100 | + basic: | ||
101 | + enabled: false | ||
102 | + # oauth2: | ||
103 | + # enabled: true | ||
104 | + # registrationId: A | ||
105 | + # userNameAttributeName: email | ||
106 | + # client: | ||
107 | + # clientName: Thingsboard Dev Test Q | ||
108 | + # clientId: 5f5c0998-1d9b-4679-9610-6108fb91af2a | ||
109 | + # clientSecret: h_kXVb7Ee1LgDDinix_nkAh_owWX7YCO783NNteF9AIOqlTWu2L03YoFjv5KL8yRVyx4uYAE-r_N3tFbupE8Kw | ||
110 | + # accessTokenUri: https://federation-q.auth.schwarz/nidp/oauth/nam/token | ||
111 | + # authorizationUri: https://federation-q.auth.schwarz/nidp/oauth/nam/authz | ||
112 | + # scope: openid,profile,email,siam | ||
113 | + # redirectUriTemplate: http://localhost:8080/login/oauth2/code/ | ||
114 | + # loginProcessingUrl: /login/oauth2/code/ | ||
115 | + # jwkSetUri: https://federation-q.auth.schwarz/nidp/oauth/nam/keys | ||
116 | + # authorizationGrantType: authorization_code # authorization_code, implicit, refresh_token, client_credentials | ||
117 | + # clientAuthenticationMethod: post # basic, post | ||
118 | + # resource: | ||
119 | + # userInfoUri: https://federation-q.auth.schwarz/nidp/oauth/nam/userinfo | ||
120 | + oauth2: | ||
121 | + enabled: true | ||
122 | + registrationId: A | ||
123 | + userNameAttributeName: email | ||
124 | + client: | ||
125 | + clientName: Test app | ||
126 | + clientId: dVH9reqyqiXIG7M2wmamb0ySue8zaM4g | ||
127 | + clientSecret: EYAfAGxwkwoeYnb2o2cDgaWZB5k97OStpZQPPvcMMD-SVH2BuughTGeBazXtF5I6 | ||
128 | + accessTokenUri: https://dev-r9m8ht0k.auth0.com/oauth/token | ||
129 | + authorizationUri: https://dev-r9m8ht0k.auth0.com/authorize | ||
130 | + scope: openid,profile,email | ||
131 | + redirectUriTemplate: http://localhost:8080/login/oauth2/code/ | ||
132 | + loginProcessingUrl: /login/oauth2/code/ | ||
133 | + jwkSetUri: https://dev-r9m8ht0k.auth0.com/.well-known/jwks.json | ||
134 | + authorizationGrantType: authorization_code # authorization_code, implicit, refresh_token, client_credentials | ||
135 | + clientAuthenticationMethod: post # basic, post | ||
136 | + resource: | ||
137 | + userInfoUri: https://dev-r9m8ht0k.auth0.com/userinfo | ||
100 | 138 | ||
101 | # Dashboard parameters | 139 | # Dashboard parameters |
102 | dashboard: | 140 | dashboard: |
@@ -459,6 +459,21 @@ | @@ -459,6 +459,21 @@ | ||
459 | <version>${spring-boot.version}</version> | 459 | <version>${spring-boot.version}</version> |
460 | </dependency> | 460 | </dependency> |
461 | <dependency> | 461 | <dependency> |
462 | + <groupId>org.springframework.cloud</groupId> | ||
463 | + <artifactId>spring-cloud-starter-oauth2</artifactId> | ||
464 | + <version>${spring-boot.version}</version> | ||
465 | + </dependency> | ||
466 | + <dependency> | ||
467 | + <groupId>org.springframework.security</groupId> | ||
468 | + <artifactId>spring-security-oauth2-client</artifactId> | ||
469 | + <version>${spring.version}</version> | ||
470 | + </dependency> | ||
471 | + <dependency> | ||
472 | + <groupId>org.springframework.security</groupId> | ||
473 | + <artifactId>spring-security-oauth2-jose</artifactId> | ||
474 | + <version>${spring.version}</version> | ||
475 | + </dependency> | ||
476 | + <dependency> | ||
462 | <groupId>org.springframework.boot</groupId> | 477 | <groupId>org.springframework.boot</groupId> |
463 | <artifactId>spring-boot-starter-web</artifactId> | 478 | <artifactId>spring-boot-starter-web</artifactId> |
464 | <version>${spring-boot.version}</version> | 479 | <version>${spring-boot.version}</version> |
@@ -47,6 +47,7 @@ | @@ -47,6 +47,7 @@ | ||
47 | </div> | 47 | </div> |
48 | </div> | 48 | </div> |
49 | <md-button class="md-raised" type="submit">{{ 'login.login' | translate }}</md-button> | 49 | <md-button class="md-raised" type="submit">{{ 'login.login' | translate }}</md-button> |
50 | + <a href="oauth2/authorization/A">OAUTH2 LOGIN</a> | ||
50 | </div> | 51 | </div> |
51 | </form> | 52 | </form> |
52 | </md-card-content> | 53 | </md-card-content> |