package org.apereo.cas.config; import org.apereo.cas.CentralAuthenticationService; import org.apereo.cas.authentication.AuthenticationSystemSupport; import org.apereo.cas.authentication.principal.DefaultPrincipalFactory; import org.apereo.cas.authentication.principal.PrincipalFactory; import org.apereo.cas.authentication.principal.Service; import org.apereo.cas.authentication.principal.ServiceFactory; import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.configuration.model.support.oauth.OAuthProperties; import org.apereo.cas.services.DenyAllAttributeReleasePolicy; import org.apereo.cas.services.RegexRegisteredService; import org.apereo.cas.services.RegisteredService; import org.apereo.cas.services.ServicesManager; import org.apereo.cas.support.oauth.authenticator.Authenticators; import org.apereo.cas.support.oauth.authenticator.OAuth20CasAuthenticationBuilder; import org.apereo.cas.support.oauth.authenticator.OAuthClientAuthenticator; import org.apereo.cas.support.oauth.authenticator.OAuthUserAuthenticator; import org.apereo.cas.support.oauth.profile.DefaultOAuth20ProfileScopeToAttributesFilter; import org.apereo.cas.support.oauth.profile.OAuth20ProfileScopeToAttributesFilter; import org.apereo.cas.support.oauth.util.OAuth20Utils; import org.apereo.cas.support.oauth.validator.OAuth20Validator; import org.apereo.cas.support.oauth.web.OAuth20CasCallbackUrlResolver; import org.apereo.cas.support.oauth.web.OAuth20HandlerInterceptorAdapter; import org.apereo.cas.support.oauth.web.endpoints.OAuth20AccessTokenEndpointController; import org.apereo.cas.support.oauth.web.endpoints.OAuth20AuthorizeEndpointController; import org.apereo.cas.support.oauth.web.endpoints.OAuth20CallbackAuthorizeEndpointController; import org.apereo.cas.support.oauth.web.endpoints.OAuth20UserProfileControllerController; import org.apereo.cas.support.oauth.web.response.OAuth20CasClientRedirectActionBuilder; import org.apereo.cas.support.oauth.web.response.OAuth20DefaultCasClientRedirectActionBuilder; import org.apereo.cas.support.oauth.web.response.accesstoken.AccessTokenResponseGenerator; import org.apereo.cas.support.oauth.web.response.accesstoken.OAuth20AccessTokenResponseGenerator; import org.apereo.cas.support.oauth.web.views.ConsentApprovalViewResolver; import org.apereo.cas.support.oauth.web.views.OAuth20CallbackAuthorizeViewResolver; import org.apereo.cas.support.oauth.web.views.OAuth20ConsentApprovalViewResolver; import org.apereo.cas.ticket.ExpirationPolicy; import org.apereo.cas.ticket.UniqueTicketIdGenerator; import org.apereo.cas.ticket.accesstoken.AccessTokenFactory; import org.apereo.cas.ticket.accesstoken.DefaultAccessTokenFactory; import org.apereo.cas.ticket.accesstoken.OAuthAccessTokenExpirationPolicy; import org.apereo.cas.ticket.code.DefaultOAuthCodeFactory; import org.apereo.cas.ticket.code.OAuthCodeExpirationPolicy; import org.apereo.cas.ticket.code.OAuthCodeFactory; import org.apereo.cas.ticket.refreshtoken.DefaultRefreshTokenFactory; import org.apereo.cas.ticket.refreshtoken.OAuthRefreshTokenExpirationPolicy; import org.apereo.cas.ticket.refreshtoken.RefreshTokenFactory; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.util.DefaultUniqueTicketIdGenerator; import org.apereo.cas.web.support.CookieRetrievingCookieGenerator; import org.pac4j.cas.client.CasClient; import org.pac4j.cas.config.CasConfiguration; import org.pac4j.core.config.Config; import org.pac4j.core.credentials.UsernamePasswordCredentials; import org.pac4j.core.credentials.authenticator.Authenticator; import org.pac4j.core.http.UrlResolver; import org.pac4j.http.client.direct.DirectBasicAuthClient; import org.pac4j.http.client.direct.DirectFormClient; import org.pac4j.springframework.web.CallbackController; import org.pac4j.springframework.web.SecurityInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.annotation.PostConstruct; import java.security.SecureRandom; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.apereo.cas.support.oauth.OAuth20Constants.*; /** * This this {@link CasOAuthConfiguration}. * * @author Misagh Moayyed * @since 5.0.0 */ @Configuration("oauthConfiguration") @EnableConfigurationProperties(CasConfigurationProperties.class) public class CasOAuthConfiguration extends WebMvcConfigurerAdapter { @Autowired @Qualifier("centralAuthenticationService") private CentralAuthenticationService centralAuthenticationService; @Autowired private CasConfigurationProperties casProperties; @Autowired @Qualifier("webApplicationServiceFactory") private ServiceFactory webApplicationServiceFactory; @Autowired @Qualifier("servicesManager") private ServicesManager servicesManager; @Autowired @Qualifier("defaultAuthenticationSystemSupport") private AuthenticationSystemSupport authenticationSystemSupport; @Autowired @Qualifier("ticketRegistry") private TicketRegistry ticketRegistry; @Autowired @Qualifier("ticketGrantingTicketCookieGenerator") private CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator; @ConditionalOnMissingBean(name = "accessTokenResponseGenerator") @Bean public AccessTokenResponseGenerator accessTokenResponseGenerator() { return new OAuth20AccessTokenResponseGenerator(); } @ConditionalOnMissingBean(name = "oauthCasClientRedirectActionBuilder") @Bean public OAuth20CasClientRedirectActionBuilder oauthCasClientRedirectActionBuilder() { return new OAuth20DefaultCasClientRedirectActionBuilder(); } @RefreshScope @Bean public UrlResolver casCallbackUrlResolver() { return new OAuth20CasCallbackUrlResolver(OAuth20Utils.casOAuthCallbackUrl(casProperties.getServer().getPrefix())); } @RefreshScope @Bean public Config oauthSecConfig() { final CasConfiguration cfg = new CasConfiguration(casProperties.getServer().getLoginUrl()); final CasClient oauthCasClient = new CasClient(cfg); oauthCasClient.setRedirectActionBuilder(webContext -> oauthCasClientRedirectActionBuilder().build(oauthCasClient, webContext)); oauthCasClient.setName(Authenticators.CAS_OAUTH_CLIENT); oauthCasClient.setUrlResolver(casCallbackUrlResolver()); final Authenticator authenticator = oAuthClientAuthenticator(); final DirectBasicAuthClient basicAuthClient = new DirectBasicAuthClient(authenticator); basicAuthClient.setName(Authenticators.CAS_OAUTH_CLIENT_BASIC_AUTHN); final DirectFormClient directFormClient = new DirectFormClient(authenticator); directFormClient.setName(Authenticators.CAS_OAUTH_CLIENT_DIRECT_FORM); directFormClient.setUsernameParameter(CLIENT_ID); directFormClient.setPasswordParameter(CLIENT_SECRET); final DirectFormClient userFormClient = new DirectFormClient(oAuthUserAuthenticator()); userFormClient.setName(Authenticators.CAS_OAUTH_CLIENT_USER_FORM); return new Config(OAuth20Utils.casOAuthCallbackUrl(casProperties.getServer().getPrefix()), oauthCasClient, basicAuthClient, directFormClient, userFormClient); } @ConditionalOnMissingBean(name = "requiresAuthenticationAuthorizeInterceptor") @Bean @RefreshScope public SecurityInterceptor requiresAuthenticationAuthorizeInterceptor() { return new SecurityInterceptor(oauthSecConfig(), Authenticators.CAS_OAUTH_CLIENT); } @ConditionalOnMissingBean(name = "consentApprovalViewResolver") @Bean @RefreshScope public ConsentApprovalViewResolver consentApprovalViewResolver() { return new OAuth20ConsentApprovalViewResolver(casProperties); } @ConditionalOnMissingBean(name = "callbackAuthorizeViewResolver") @Bean @RefreshScope public OAuth20CallbackAuthorizeViewResolver callbackAuthorizeViewResolver() { return new OAuth20CallbackAuthorizeViewResolver() { }; } @ConditionalOnMissingBean(name = "requiresAuthenticationAccessTokenInterceptor") @Bean @RefreshScope public SecurityInterceptor requiresAuthenticationAccessTokenInterceptor() { final String clients = Stream.of(Authenticators.CAS_OAUTH_CLIENT_BASIC_AUTHN, Authenticators.CAS_OAUTH_CLIENT_DIRECT_FORM, Authenticators.CAS_OAUTH_CLIENT_USER_FORM).collect(Collectors.joining(",")); return new SecurityInterceptor(oauthSecConfig(), clients); } @ConditionalOnMissingBean(name = "oauthInterceptor") @Bean @RefreshScope public HandlerInterceptorAdapter oauthInterceptor() { return new OAuth20HandlerInterceptorAdapter(requiresAuthenticationAccessTokenInterceptor(), requiresAuthenticationAuthorizeInterceptor()); } @Override public void addInterceptors(final InterceptorRegistry registry) { registry.addInterceptor(oauthInterceptor()).addPathPatterns(BASE_OAUTH20_URL.concat("/").concat("*")); } @Bean @RefreshScope public OAuth20CasClientRedirectActionBuilder defaultOAuthCasClientRedirectActionBuilder() { return new OAuth20DefaultCasClientRedirectActionBuilder(); } @ConditionalOnMissingBean(name = "oAuthClientAuthenticator") @Bean @RefreshScope public Authenticator<UsernamePasswordCredentials> oAuthClientAuthenticator() { return new OAuthClientAuthenticator(oAuthValidator(), this.servicesManager); } @ConditionalOnMissingBean(name = "oAuthUserAuthenticator") @Bean @RefreshScope public Authenticator<UsernamePasswordCredentials> oAuthUserAuthenticator() { return new OAuthUserAuthenticator(authenticationSystemSupport, servicesManager, webApplicationServiceFactory); } @ConditionalOnMissingBean(name = "oAuthValidator") @Bean @RefreshScope public OAuth20Validator oAuthValidator() { return new OAuth20Validator(webApplicationServiceFactory); } @ConditionalOnMissingBean(name = "oauthAccessTokenResponseGenerator") @Bean @RefreshScope public AccessTokenResponseGenerator oauthAccessTokenResponseGenerator() { return new OAuth20AccessTokenResponseGenerator(); } @Bean @RefreshScope @ConditionalOnMissingBean(name = "defaultAccessTokenFactory") public AccessTokenFactory defaultAccessTokenFactory() { return new DefaultAccessTokenFactory(accessTokenIdGenerator(), accessTokenExpirationPolicy()); } private ExpirationPolicy accessTokenExpirationPolicy() { final OAuthProperties oauth = casProperties.getAuthn().getOauth(); return new OAuthAccessTokenExpirationPolicy( oauth.getAccessToken().getMaxTimeToLiveInSeconds(), oauth.getAccessToken().getTimeToKillInSeconds() ); } private ExpirationPolicy oAuthCodeExpirationPolicy() { final OAuthProperties oauth = casProperties.getAuthn().getOauth(); return new OAuthCodeExpirationPolicy(oauth.getCode().getNumberOfUses(), oauth.getCode().getTimeToKillInSeconds()); } @Bean @RefreshScope public UniqueTicketIdGenerator oAuthCodeIdGenerator() { return new DefaultUniqueTicketIdGenerator(); } @Bean @RefreshScope public UniqueTicketIdGenerator refreshTokenIdGenerator() { return new DefaultUniqueTicketIdGenerator(); } @Bean @RefreshScope @ConditionalOnMissingBean(name = "defaultOAuthCodeFactory") public OAuthCodeFactory defaultOAuthCodeFactory() { return new DefaultOAuthCodeFactory(oAuthCodeIdGenerator(), oAuthCodeExpirationPolicy()); } @ConditionalOnMissingBean(name = "profileScopeToAttributesFilter") @Bean public OAuth20ProfileScopeToAttributesFilter profileScopeToAttributesFilter() { return new DefaultOAuth20ProfileScopeToAttributesFilter(); } @Bean @ConditionalOnMissingBean(name = "callbackAuthorizeController") @RefreshScope public OAuth20CallbackAuthorizeEndpointController callbackAuthorizeController() { return new OAuth20CallbackAuthorizeEndpointController(servicesManager, ticketRegistry, oAuthValidator(), defaultAccessTokenFactory(), oauthPrincipalFactory(), webApplicationServiceFactory, oauthSecConfig(), callbackController(), callbackAuthorizeViewResolver(), profileScopeToAttributesFilter(), casProperties, ticketGrantingTicketCookieGenerator); } @ConditionalOnMissingBean(name = "accessTokenController") @Bean @RefreshScope public OAuth20AccessTokenEndpointController accessTokenController() { return new OAuth20AccessTokenEndpointController( servicesManager, ticketRegistry, oAuthValidator(), defaultAccessTokenFactory(), oauthPrincipalFactory(), webApplicationServiceFactory, defaultRefreshTokenFactory(), accessTokenResponseGenerator(), profileScopeToAttributesFilter(), casProperties, ticketGrantingTicketCookieGenerator, oauthCasAuthenticationBuilder(), centralAuthenticationService ); } @ConditionalOnMissingBean(name = "profileController") @Bean @RefreshScope public OAuth20UserProfileControllerController profileController() { return new OAuth20UserProfileControllerController(servicesManager, ticketRegistry, oAuthValidator(), defaultAccessTokenFactory(), oauthPrincipalFactory(), webApplicationServiceFactory, profileScopeToAttributesFilter(), casProperties, ticketGrantingTicketCookieGenerator); } @ConditionalOnMissingBean(name = "authorizeController") @Bean @RefreshScope public OAuth20AuthorizeEndpointController authorizeController() { return new OAuth20AuthorizeEndpointController( servicesManager, ticketRegistry, oAuthValidator(), defaultAccessTokenFactory(), oauthPrincipalFactory(), webApplicationServiceFactory, defaultOAuthCodeFactory(), consentApprovalViewResolver(), profileScopeToAttributesFilter(), casProperties, ticketGrantingTicketCookieGenerator, oauthCasAuthenticationBuilder() ); } @ConditionalOnMissingBean(name = "oauthPrincipalFactory") @Bean @RefreshScope public PrincipalFactory oauthPrincipalFactory() { return new DefaultPrincipalFactory(); } @Bean @RefreshScope @ConditionalOnMissingBean(name = "defaultRefreshTokenFactory") public RefreshTokenFactory defaultRefreshTokenFactory() { return new DefaultRefreshTokenFactory(refreshTokenIdGenerator(), refreshTokenExpirationPolicy()); } private ExpirationPolicy refreshTokenExpirationPolicy() { return new OAuthRefreshTokenExpirationPolicy(casProperties.getAuthn().getOauth().getRefreshToken().getTimeToKillInSeconds()); } @ConditionalOnMissingBean(name = "oauthCasAuthenticationBuilder") @Bean @RefreshScope public OAuth20CasAuthenticationBuilder oauthCasAuthenticationBuilder() { return new OAuth20CasAuthenticationBuilder(oauthPrincipalFactory(), webApplicationServiceFactory, profileScopeToAttributesFilter(), casProperties); } @Bean @RefreshScope public CallbackController callbackController() { final CallbackController c = new CallbackController(); c.setConfig(oauthSecConfig()); return c; } @ConditionalOnMissingBean(name = "accessTokenIdGenerator") @Bean @RefreshScope public UniqueTicketIdGenerator accessTokenIdGenerator() { return new DefaultUniqueTicketIdGenerator(); } @PostConstruct public void initializeServletApplicationContext() { final String oAuthCallbackUrl = casProperties.getServer().getPrefix() + BASE_OAUTH20_URL + '/' + CALLBACK_AUTHORIZE_URL_DEFINITION; final Service callbackService = this.webApplicationServiceFactory.createService(oAuthCallbackUrl); final RegisteredService svc = servicesManager.findServiceBy(callbackService); if (svc == null || !svc.getServiceId().equals(oAuthCallbackUrl)) { final RegexRegisteredService service = new RegexRegisteredService(); service.setId(Math.abs(new SecureRandom().nextLong())); service.setEvaluationOrder(0); service.setName(service.getClass().getSimpleName()); service.setDescription("OAuth Authentication Callback Request URL"); service.setServiceId(oAuthCallbackUrl); service.setAttributeReleasePolicy(new DenyAllAttributeReleasePolicy()); servicesManager.save(service); servicesManager.load(); } } }