Skip to content

Commit fa9669a

Browse files
author
Mustafa BOLEKEN
authored
Merge pull request #508 from ant-media/keyCloakIntegration
Key cloak integration
2 parents 0374eeb + 64e7bb9 commit fa9669a

File tree

4 files changed

+266
-2
lines changed

4 files changed

+266
-2
lines changed

pom.xml

+47
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,53 @@
229229
<version>${jersey.version}</version>
230230
<scope>provided</scope>
231231
</dependency>
232+
233+
234+
<dependency>
235+
<groupId>org.springframework.security</groupId>
236+
<artifactId>spring-security-core</artifactId>
237+
<version>6.2.0</version>
238+
</dependency>
239+
<dependency>
240+
<groupId>org.springframework.security</groupId>
241+
<artifactId>spring-security-config</artifactId>
242+
<version>6.2.0</version>
243+
</dependency>
244+
<dependency>
245+
<groupId>org.springframework.security</groupId>
246+
<artifactId>spring-security-web</artifactId>
247+
<version>6.2.0</version>
248+
</dependency>
249+
<dependency>
250+
<groupId>org.springframework.security</groupId>
251+
<artifactId>spring-security-crypto</artifactId>
252+
<version>6.2.0</version>
253+
</dependency>
254+
<dependency>
255+
<groupId>org.springframework.security</groupId>
256+
<artifactId>spring-security-oauth2-authorization-server</artifactId>
257+
<version>1.1.6</version>
258+
</dependency>
259+
260+
261+
262+
263+
<!-- OAuth2 dependencies -->
264+
<dependency>
265+
<groupId>org.springframework.security.oauth</groupId>
266+
<artifactId>spring-security-oauth2</artifactId>
267+
<version>2.5.2.RELEASE</version>
268+
</dependency>
269+
<dependency>
270+
<groupId>org.springframework.security</groupId>
271+
<artifactId>spring-security-oauth2-client</artifactId>
272+
<version>6.2.0</version>
273+
</dependency>
274+
<dependency>
275+
<groupId>org.springframework.security</groupId>
276+
<artifactId>spring-security-oauth2-jose</artifactId>
277+
<version>6.2.0</version>
278+
</dependency>
232279

233280
<dependency>
234281
<groupId>junit</groupId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package io.antmedia;
2+
3+
import java.util.Collection;
4+
import java.util.HashSet;
5+
import java.util.Map;
6+
import java.util.Set;
7+
import java.util.stream.Collectors;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.http.HttpMethod;
10+
import org.springframework.security.config.Customizer;
11+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
12+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
13+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
14+
import org.springframework.security.core.GrantedAuthority;
15+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
16+
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
17+
import org.springframework.security.core.session.SessionRegistry;
18+
import org.springframework.security.core.session.SessionRegistryImpl;
19+
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
20+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
21+
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
22+
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
23+
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
24+
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
25+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
26+
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
27+
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
28+
import org.springframework.security.oauth2.jwt.JwtDecoder;
29+
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
30+
import org.springframework.security.web.SecurityFilterChain;
31+
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
32+
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
33+
import org.springframework.security.web.session.HttpSessionEventPublisher;
34+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
35+
36+
@EnableWebSecurity
37+
class SecurityConfiguration {
38+
39+
private static final String GROUPS = "groups";
40+
private static final String REALM_ACCESS_CLAIM = "realm_access";
41+
private static final String ROLES_CLAIM = "roles";
42+
43+
/*
44+
* Realm URL in this format for keycloak
45+
* "http://localhost:8080/realms/AntMedia";
46+
*/
47+
private String realmUrl = null;
48+
49+
/*
50+
* webapp name like: LiveApp, live
51+
*/
52+
private String appName = null;
53+
private String clientId = "stream-application"; //"stream-application";
54+
private String role = "user";
55+
56+
57+
@Bean
58+
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
59+
http.authorizeHttpRequests(auth -> auth
60+
// Allows preflight requests from browser for any paths under /appName/**
61+
.requestMatchers(new AntPathRequestMatcher("/**", HttpMethod.OPTIONS.name()))
62+
.permitAll()
63+
// Allow access without authentication to /appName/rest/**
64+
.requestMatchers(new AntPathRequestMatcher("/rest/**"))
65+
.permitAll()
66+
// Require authentication for all other paths under /appName/**
67+
.requestMatchers(new AntPathRequestMatcher("/" + appName + "/**"))
68+
.hasRole(role)
69+
// Permit access to the root URL
70+
.requestMatchers(new AntPathRequestMatcher("/"))
71+
.permitAll()
72+
// Authenticate any other request
73+
.anyRequest().authenticated()
74+
);
75+
76+
//for vod upload
77+
http.csrf(AbstractHttpConfigurer::disable);
78+
79+
// Configure the resource server to use JWT authentication
80+
http.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
81+
82+
// Configure OAuth2 login
83+
http.oauth2Login(Customizer.withDefaults());
84+
85+
return http.build();
86+
}
87+
88+
89+
@Bean
90+
public GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() {
91+
return authorities -> {
92+
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
93+
var authority = authorities.iterator().next();
94+
boolean isOidc = authority instanceof OidcUserAuthority;
95+
96+
if (isOidc) {
97+
var oidcUserAuthority = (OidcUserAuthority) authority;
98+
var userInfo = oidcUserAuthority.getUserInfo();
99+
100+
// Tokens can be configured to return roles under
101+
// Groups or REALM ACCESS hence have to check both
102+
if (userInfo.hasClaim(REALM_ACCESS_CLAIM)) {
103+
var realmAccess = userInfo.getClaimAsMap(REALM_ACCESS_CLAIM);
104+
var roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
105+
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
106+
} else if (userInfo.hasClaim(GROUPS)) {
107+
Collection<String> roles = userInfo.getClaim(GROUPS);
108+
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
109+
}
110+
} else {
111+
var oauth2UserAuthority = (OAuth2UserAuthority) authority;
112+
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
113+
114+
if (userAttributes.containsKey(REALM_ACCESS_CLAIM)) {
115+
Map<String, Object> realmAccess = (Map<String, Object>) userAttributes.get(REALM_ACCESS_CLAIM);
116+
Collection<String> roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
117+
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
118+
}
119+
}
120+
return mappedAuthorities;
121+
};
122+
}
123+
124+
@Bean
125+
public SessionRegistry sessionRegistry() {
126+
return new SessionRegistryImpl();
127+
}
128+
129+
@Bean
130+
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
131+
return new RegisterSessionAuthenticationStrategy(sessionRegistry());
132+
}
133+
134+
@Bean
135+
public HttpSessionEventPublisher httpSessionEventPublisher() {
136+
return new HttpSessionEventPublisher();
137+
}
138+
139+
Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
140+
return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(
141+
Collectors.toList());
142+
}
143+
144+
@Bean
145+
public JwtDecoder jwtDecoder() {
146+
return NimbusJwtDecoder.withJwkSetUri(realmUrl + "/protocol/openid-connect/certs").build();
147+
}
148+
149+
@Bean
150+
public ClientRegistrationRepository clientRegistrationRepository() {
151+
return new InMemoryClientRegistrationRepository(keycloakClientRegistration());
152+
}
153+
154+
@Bean
155+
public OAuth2AuthorizationRequestResolver authorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
156+
return new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, "/oauth2/authorization");
157+
}
158+
159+
@Bean
160+
public OidcUserService oidcUserService() {
161+
return new OidcUserService();
162+
}
163+
164+
@Bean
165+
public ClientRegistration keycloakClientRegistration() {
166+
return ClientRegistration.withRegistrationId("keycloak")
167+
.clientId(clientId)
168+
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
169+
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
170+
//.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
171+
.scope("openid")
172+
.authorizationUri(realmUrl + "/protocol/openid-connect/auth")
173+
.tokenUri(realmUrl + "/protocol/openid-connect/token")
174+
.userInfoUri(realmUrl + "/protocol/openid-connect/userinfo")
175+
.userNameAttributeName("preferred_username")
176+
.jwkSetUri(realmUrl + "/protocol/openid-connect/certs")
177+
.clientName("Keycloak")
178+
.build();
179+
}
180+
181+
182+
public void setRealmUrl(String realmUrl) {
183+
this.realmUrl = realmUrl;
184+
}
185+
public void setAppName(String appName) {
186+
this.appName = appName;
187+
}
188+
public void setClientId(String clientId) {
189+
this.clientId = clientId;
190+
}
191+
public void setRole(String role) {
192+
this.role = role;
193+
}
194+
}

src/main/webapp/WEB-INF/red5-web.xml

+11-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,17 @@
1616
<!--- S3 Recording info -->
1717
<bean id="app.storageClient" class="io.antmedia.storage.AmazonS3StorageClient">
1818
</bean>
19-
19+
20+
<!-- For Keycloak Integration -->
21+
<!--
22+
<bean id="openid.config" class="io.antmedia.SecurityConfiguration">
23+
<property name="realmUrl" value="http://keycloak.antmedia.cloud:8080/realms/antmedia" />
24+
<property name="appName" value="demo" />
25+
<property name="clientId" value="stream-application" />
26+
<property name="role" value="user" />
27+
</bean>
28+
-->
29+
2030
<bean id="app.settings" class="io.antmedia.AppSettings" />
2131

2232
<!-- Defines the web context -->

src/main/webapp/WEB-INF/web.xml

+14-1
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,20 @@
2626
<param-value>true</param-value>
2727
</context-param>
2828

29-
3029
<listener>
3130
<listener-class>io.antmedia.filter.TokenSessionFilter</listener-class>
3231
</listener>
32+
33+
<filter>
34+
<filter-name>springSecurityFilterChain</filter-name>
35+
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
36+
</filter>
37+
<filter-mapping>
38+
<filter-name>springSecurityFilterChain</filter-name>
39+
<url-pattern>/*</url-pattern>
40+
</filter-mapping>
41+
42+
3343
<!-- Put RestProxyFilter to the top of the Filter list -->
3444
<filter>
3545
<filter-name>RestProxyFilter</filter-name>
@@ -277,6 +287,8 @@
277287
<url-pattern>/rest/*</url-pattern>
278288
</filter-mapping>
279289

290+
<!-- For Keycloak Integration -->
291+
<!--
280292
<filter>
281293
<filter-name>ContentSecurityPolicyHeaderFilter</filter-name>
282294
<filter-class>io.antmedia.filter.ContentSecurityPolicyHeaderFilter</filter-class>
@@ -286,6 +298,7 @@
286298
<filter-name>ContentSecurityPolicyHeaderFilter</filter-name>
287299
<url-pattern>/*</url-pattern>
288300
</filter-mapping>
301+
-->
289302

290303
<filter>
291304
<filter-name>IPFilter</filter-name>

0 commit comments

Comments
 (0)