Skip to content

Commit 691c7a8

Browse files
authored
feat: scrap for each Oauth2 providers favicon (#403)
1 parent e0ee407 commit 691c7a8

3 files changed

Lines changed: 215 additions & 32 deletions

File tree

server/src/main/java/dev/findfirst/users/controller/UserController.java

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import java.net.URISyntaxException;
77
import java.nio.file.Files;
88
import java.rmi.UnexpectedException;
9-
import java.util.ArrayList;
109
import java.util.Arrays;
1110
import java.util.List;
1211

@@ -32,12 +31,12 @@
3231
import dev.findfirst.users.model.user.TokenPassword;
3332
import dev.findfirst.users.model.user.User;
3433
import dev.findfirst.users.service.ForgotPasswordService;
34+
import dev.findfirst.users.service.Oauth2SourceService;
3535
import dev.findfirst.users.service.RegistrationService;
3636
import dev.findfirst.users.service.UserManagementService;
3737

3838
import lombok.RequiredArgsConstructor;
3939
import lombok.extern.slf4j.Slf4j;
40-
import org.springframework.beans.factory.annotation.Autowired;
4140
import org.springframework.beans.factory.annotation.Value;
4241
import org.springframework.core.io.FileSystemResource;
4342
import org.springframework.core.io.Resource;
@@ -46,7 +45,6 @@
4645
import org.springframework.http.MediaType;
4746
import org.springframework.http.ResponseCookie;
4847
import org.springframework.http.ResponseEntity;
49-
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
5048
import org.springframework.web.bind.annotation.*;
5149
import org.springframework.web.multipart.MultipartFile;
5250

@@ -65,12 +63,8 @@ public class UserController {
6563

6664
private final RefreshTokenService refreshTokenService;
6765

68-
private InMemoryClientRegistrationRepository oauth2Providers;
66+
private final Oauth2SourceService oauth2SourceService;
6967

70-
@Autowired(required = false)
71-
public void setOauth2(InMemoryClientRegistrationRepository clients) {
72-
this.oauth2Providers = clients;
73-
}
7468

7569
@Value("${findfirst.app.frontend-url}")
7670
private String frontendUrl;
@@ -93,30 +87,7 @@ public ResponseEntity<User> userInfo() throws NoUserFoundException {
9387

9488
@GetMapping("/oauth2Providers")
9589
public ResponseEntity<List<Oauth2Source>> oauth2Providers() {
96-
List<Oauth2Source> listOfAuth2Providers = new ArrayList<>();
97-
if (oauth2Providers == null) {
98-
return ResponseEntity.ofNullable(listOfAuth2Providers);
99-
}
100-
oauth2Providers.iterator().forEachRemaining(provider -> {
101-
var tknUri = provider.getProviderDetails().getTokenUri();
102-
log.debug("Token URI {}", tknUri);
103-
// skip http(s)://
104-
var noProto = "";
105-
if (tknUri.contains("https://")) {
106-
noProto = tknUri.substring(8);
107-
} else {
108-
log.debug("provider without https {}", tknUri);
109-
// do we really want to trust anything that isn't https?
110-
return;
111-
}
112-
var favDomain = noProto.indexOf("/");
113-
var faviconURI = "https://" + noProto.substring(0, favDomain) + "/favicon.ico";
114-
var registrationId = provider.getRegistrationId();
115-
log.debug("Favicon URI {}", faviconURI);
116-
listOfAuth2Providers.add(new Oauth2Source(provider.getClientName(), faviconURI,
117-
"oauth2/authorization/" + registrationId));
118-
});
119-
return ResponseEntity.ofNullable(listOfAuth2Providers);
90+
return ResponseEntity.ok(oauth2SourceService.oauth2Sources());
12091
}
12192

12293
@PostMapping("/signup")
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package dev.findfirst.users.service;
2+
3+
import java.io.IOException;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
7+
import jakarta.annotation.PostConstruct;
8+
9+
import dev.findfirst.users.model.oauth2.Oauth2Source;
10+
11+
import lombok.RequiredArgsConstructor;
12+
import lombok.extern.slf4j.Slf4j;
13+
import org.jsoup.Jsoup;
14+
import org.jsoup.nodes.Element;
15+
import org.springframework.beans.factory.annotation.Autowired;
16+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
17+
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
18+
import org.springframework.stereotype.Service;
19+
20+
@Service
21+
@RequiredArgsConstructor
22+
@Slf4j
23+
public class Oauth2SourceService {
24+
25+
private InMemoryClientRegistrationRepository oauth2Providers;
26+
27+
private final List<Oauth2Source> oauth2Sources = new ArrayList<>();
28+
29+
@PostConstruct
30+
void init() {
31+
oauth2Providers.iterator().forEachRemaining(provider -> {
32+
var tknUri = provider.getProviderDetails().getTokenUri();
33+
log.debug("Token URI {}", tknUri);
34+
// skip http(s)://
35+
if (!tknUri.contains("https://")) {
36+
log.debug("provider without https {}", tknUri);
37+
// do we really want to trust anything that isn't https?
38+
return;
39+
}
40+
oauth2Sources.add(new Oauth2Source(provider.getClientName(), getFaviconURI(provider),
41+
"oauth2/authorization/" + provider.getRegistrationId()));
42+
});
43+
}
44+
45+
public List<Oauth2Source> oauth2Sources() {
46+
return oauth2Sources;
47+
}
48+
49+
@Autowired(required = false)
50+
public void setOauth2Providers(InMemoryClientRegistrationRepository oauth2Providers) {
51+
this.oauth2Providers = oauth2Providers;
52+
}
53+
54+
private String getFaviconURI(ClientRegistration provider) {
55+
var targetUri = provider.getProviderDetails().getAuthorizationUri();
56+
log.debug("Scraping {} for favicon URI", targetUri);
57+
try {
58+
Element link = Jsoup.connect(targetUri).userAgent("Mozilla").get().head()
59+
.select("link[href~=.*\\.(ico|png)]").first();
60+
61+
if (link == null) {
62+
log.debug("No favicon URI found at {}", targetUri);
63+
return null;
64+
}
65+
66+
String href = link.attr("href");
67+
log.debug("Found favicon URI: {}", href);
68+
return href;
69+
} catch (IOException e) {
70+
log.error("Failed to scrape {}", targetUri, e);
71+
return null;
72+
}
73+
74+
}
75+
76+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package dev.findfirst.users.service;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
import static org.mockito.Mockito.*;
5+
6+
import java.io.IOException;
7+
import java.util.List;
8+
9+
import dev.findfirst.users.model.oauth2.Oauth2Source;
10+
11+
import org.jsoup.Connection;
12+
import org.jsoup.Jsoup;
13+
import org.jsoup.nodes.Document;
14+
import org.jsoup.nodes.Element;
15+
import org.jsoup.select.Elements;
16+
import org.junit.jupiter.api.BeforeEach;
17+
import org.junit.jupiter.api.Test;
18+
import org.mockito.MockedStatic;
19+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
20+
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
21+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
22+
23+
class Oauth2SourceServiceTest {
24+
25+
private Oauth2SourceService service;
26+
27+
private ClientRegistration provider;
28+
29+
@BeforeEach
30+
void setUp() {
31+
provider = ClientRegistration.withRegistrationId("test").clientName("Test Provider")
32+
.authorizationUri("https://www.example-auth.com")
33+
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
34+
.tokenUri("https://www.example.com").build();
35+
var providers = new InMemoryClientRegistrationRepository(List.of(provider));
36+
service = new Oauth2SourceService();
37+
service.setOauth2Providers(providers);
38+
}
39+
40+
@Test
41+
void testReturnsOauth2SourcesWithIconUrl_WhenFoundedAtWebsite() throws IOException {
42+
try (MockedStatic<Jsoup> jsoupMock = mockStatic(Jsoup.class)) {
43+
Connection mockConnection = mock(Connection.class);
44+
Document mockDoc = mock(Document.class);
45+
Element mockElement = mock(Element.class);
46+
Elements mockElements = mock(Elements.class);
47+
48+
when(mockConnection.userAgent(anyString())).thenReturn(mockConnection);
49+
when(mockConnection.get()).thenReturn(mockDoc);
50+
when(mockDoc.head()).thenReturn(mockElement);
51+
when(mockElement.select(anyString())).thenReturn(mockElements);
52+
when(mockElements.first()).thenReturn(mockElement);
53+
when(mockElement.attr("href")).thenReturn("https://example.com/assets/favicon.ico");
54+
jsoupMock.when(() -> Jsoup.connect(anyString())).thenReturn(mockConnection);
55+
56+
service.init();
57+
58+
List<Oauth2Source> oauth2Sources = service.oauth2Sources();
59+
60+
assertFalse(oauth2Sources.isEmpty());
61+
var oauth2Source = oauth2Sources.getFirst();
62+
assertEquals(provider.getClientName(), oauth2Source.provider());
63+
assertEquals("https://example.com/assets/favicon.ico", oauth2Source.iconUrl());
64+
assertEquals("oauth2/authorization/" + provider.getRegistrationId(),
65+
oauth2Source.authEndpoint());
66+
67+
68+
}
69+
}
70+
71+
@Test
72+
void testReturnsOauth2SourcesWithoutIconUrl_WhenNotFoundedAtWebsite() throws IOException {
73+
try (MockedStatic<Jsoup> jsoupMock = mockStatic(Jsoup.class)) {
74+
Connection mockConnection = mock(Connection.class);
75+
Document mockDoc = mock(Document.class);
76+
Element mockElement = mock(Element.class);
77+
Elements mockElements = mock(Elements.class);
78+
79+
when(mockConnection.userAgent(anyString())).thenReturn(mockConnection);
80+
when(mockConnection.get()).thenReturn(mockDoc);
81+
when(mockDoc.head()).thenReturn(mockElement);
82+
when(mockElement.select(anyString())).thenReturn(mockElements);
83+
when(mockElements.first()).thenReturn(null);
84+
jsoupMock.when(() -> Jsoup.connect(anyString())).thenReturn(mockConnection);
85+
86+
service.init();
87+
88+
List<Oauth2Source> oauth2Sources = service.oauth2Sources();
89+
90+
assertFalse(oauth2Sources.isEmpty());
91+
var oauth2Source = oauth2Sources.getFirst();
92+
assertEquals(provider.getClientName(), oauth2Source.provider());
93+
assertNull(oauth2Source.iconUrl());
94+
assertEquals("oauth2/authorization/" + provider.getRegistrationId(),
95+
oauth2Source.authEndpoint());
96+
97+
98+
}
99+
}
100+
101+
@Test
102+
void testReturnsOauth2SourcesWithoutIconUrl_WhenNotConnectedAtWebsite() throws IOException {
103+
try (MockedStatic<Jsoup> jsoupMock = mockStatic(Jsoup.class)) {
104+
Connection mockConnection = mock(Connection.class);
105+
when(mockConnection.userAgent(anyString())).thenReturn(mockConnection);
106+
when(mockConnection.get()).thenThrow(IOException.class);
107+
108+
jsoupMock.when(() -> Jsoup.connect(anyString())).thenReturn(mockConnection);
109+
110+
service.init();
111+
112+
List<Oauth2Source> oauth2Sources = service.oauth2Sources();
113+
114+
assertFalse(oauth2Sources.isEmpty());
115+
var oauth2Source = oauth2Sources.getFirst();
116+
assertEquals(provider.getClientName(), oauth2Source.provider());
117+
assertNull(oauth2Source.iconUrl());
118+
assertEquals("oauth2/authorization/" + provider.getRegistrationId(),
119+
oauth2Source.authEndpoint());
120+
121+
}
122+
}
123+
124+
@Test
125+
void testReturnsOauth2SourcesWithoutUntrustedTokenUri() {
126+
provider = ClientRegistration.withRegistrationId("test").clientName("Test Provider")
127+
.authorizationUri("https://www.example-auth.com")
128+
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
129+
.tokenUri("http://www.example.com").build();
130+
var providersWithoutSSL = new InMemoryClientRegistrationRepository(List.of(provider));
131+
service.setOauth2Providers(providersWithoutSSL);
132+
service.init();
133+
assertTrue(service.oauth2Sources().isEmpty());
134+
135+
}
136+
}

0 commit comments

Comments
 (0)