SAMLAuthenticationProvider

/* Copyright 2009 Vladimir Schafer
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.security.saml;

import org.joda.time.DateTime;
import org.opensaml.common.SAMLException;
import org.opensaml.common.SAMLRuntimeException;
import org.opensaml.saml2.core.AuthnStatement;
import org.opensaml.xml.encryption.DecryptionException;
import org.opensaml.xml.validation.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.providers.ExpiringUsernameAuthenticationToken;
import org.springframework.security.saml.context.SAMLMessageContext;
import org.springframework.security.saml.log.SAMLLogger;
import org.springframework.security.saml.userdetails.SAMLUserDetailsService;
import org.springframework.security.saml.websso.WebSSOProfileConsumer;
import org.springframework.util.Assert;

import javax.servlet.ServletException;
import java.util.*;

/**
 * Authentication provider is capable of verifying validity of a SAMLAuthenticationToken and in case
 * the token is valid to create an authenticated UsernamePasswordAuthenticationToken.
 *
 * @author Vladimir Schafer
 */
public class SAMLAuthenticationProvider implements AuthenticationProvider, InitializingBean {

    private static final Logger log = LoggerFactory.getLogger(SAMLAuthenticationProvider.class);

    private boolean forcePrincipalAsString = true;
    private boolean excludeCredential = false;
    protected WebSSOProfileConsumer consumer;
    protected WebSSOProfileConsumer hokConsumer;
    protected SAMLLogger samlLogger;
    protected SAMLUserDetailsService userDetails;

    /**
     * Attempts to perform authentication of an Authentication object. The authentication must be of type
     * SAMLAuthenticationToken and must contain filled SAMLMessageContext. If the SAML inbound message
     * in the context is valid, UsernamePasswordAuthenticationToken with name given in the SAML message NameID
     * and assertion used to verify the user as credential (SAMLCredential object) is created and set as authenticated.
     *
     * @param authentication SAMLAuthenticationToken to verify
     * @return UsernamePasswordAuthenticationToken with name as NameID value and SAMLCredential as credential object
     * @throws AuthenticationException user can't be authenticated due to an error
     */
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        if (!supports(authentication.getClass())) {
            throw new IllegalArgumentException("Only SAMLAuthenticationToken is supported, " + authentication.getClass() + " was attempted");
        }

        SAMLAuthenticationToken token = (SAMLAuthenticationToken) authentication;
        SAMLMessageContext context = token.getCredentials();

        if (context == null) {
            throw new AuthenticationServiceException("SAML message context is not available in the authentication token");
        }

        SAMLCredential credential;

        try {
            if (SAMLConstants.SAML2_WEBSSO_PROFILE_URI.equals(context.getCommunicationProfileId())) {
                credential = consumer.processAuthenticationResponse(context);
            } else if (SAMLConstants.SAML2_HOK_WEBSSO_PROFILE_URI.equals(context.getCommunicationProfileId())) {
                credential = hokConsumer.processAuthenticationResponse(context);
            } else {
                throw new SAMLException("Unsupported profile encountered in the context " + context.getCommunicationProfileId());
            }
        } catch (SAMLRuntimeException e) {
            log.debug("Error validating SAML message", e);
            samlLogger.log(SAMLConstants.AUTH_N_RESPONSE, SAMLConstants.FAILURE, context, e);
            throw new AuthenticationServiceException("Error validating SAML message", e);
        } catch (SAMLException e) {
            log.debug("Error validating SAML message", e);
            samlLogger.log(SAMLConstants.AUTH_N_RESPONSE, SAMLConstants.FAILURE, context, e);
            throw new AuthenticationServiceException("Error validating SAML message", e);
        } catch (ValidationException e) {
            log.debug("Error validating signature", e);
            samlLogger.log(SAMLConstants.AUTH_N_RESPONSE, SAMLConstants.FAILURE, context, e);
            throw new AuthenticationServiceException("Error validating SAML message signature", e);
        } catch (org.opensaml.xml.security.SecurityException e) {
            log.debug("Error validating signature", e);
            samlLogger.log(SAMLConstants.AUTH_N_RESPONSE, SAMLConstants.FAILURE, context, e);
            throw new AuthenticationServiceException("Error validating SAML message signature", e);
        } catch (DecryptionException e) {
            log.debug("Error decrypting SAML message", e);
            samlLogger.log(SAMLConstants.AUTH_N_RESPONSE, SAMLConstants.FAILURE, context, e);
            throw new AuthenticationServiceException("Error decrypting SAML message", e);
        }

        Object userDetails = getUserDetails(credential);
        Object principal = getPrincipal(credential, userDetails);
        Collection<? extends GrantedAuthority> entitlements = getEntitlements(credential, userDetails);

        Date expiration = getExpirationDate(credential);

        SAMLCredential authenticationCredential = excludeCredential ? null : credential;
        ExpiringUsernameAuthenticationToken result = new ExpiringUsernameAuthenticationToken(expiration, principal, authenticationCredential, entitlements);
        result.setDetails(userDetails);

        samlLogger.log(SAMLConstants.AUTH_N_RESPONSE, SAMLConstants.SUCCESS, context, result, null);

        return result;

    }

    /**
     * Populates user data from SAMLCredential into UserDetails object. By default supplied implementation of the
     * SAMLUserDetailsService is called and value of type UserDetails is returned. Users are encouraged to supply
     * implementation of this class and also include correct implementation of the getAuthorities method in it, which
     * is used to populate the entitlements inside the Authentication object.
     * <p>
     * If no SAMLUserDetailsService is specified null is returned.
     *
     * @param credential credential to load user from
     * @return user details object corresponding to the SAML credential or null if data can't be loaded
     */
    protected Object getUserDetails(SAMLCredential credential) {
        if (getUserDetails() != null) {
            return getUserDetails().loadUserBySAML(credential);
        } else {
            return null;
        }
    }

    /**
     * Method determines what will be stored as principal of the created Authentication object. By default
     * (when forcePrincipalAsString is true) string representation of the NameID returned from SAML message is used.
     * Otherwise userDetail object is used, when set, when not NameID object from the credential is returned.
     * Other implementations can be created by overriding the method.
     *
     * @param credential credential used to authenticate user
     * @param userDetail loaded user details, can be null
     * @return principal to store inside Authentication object
     */
    protected Object getPrincipal(SAMLCredential credential, Object userDetail) {
        if (isForcePrincipalAsString()) {
            return credential.getNameID().getValue();
        } else if (userDetail != null) {
            return userDetail;
        } else {
            return credential.getNameID();
        }
    }

    /**
     * Method is responsible for returning collection of users entitlements. Default implementation verifies
     * whether userDetail object is of UserDetails type and returns userDetail.getAuthorities().
     * <p>
     * In case object of other type is found empty list is returned. Users are supposed to override this
     * method to provide custom parsing is such case.
     *
     * @param credential credential used to authenticate user during SSO
     * @param userDetail user detail object returned from getUserDetails call
     * @return collection of users entitlements, mustn't be null
     */
    protected Collection<? extends GrantedAuthority> getEntitlements(SAMLCredential credential, Object userDetail) {
        if (userDetail instanceof UserDetails) {
            List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
            authorities.addAll(((UserDetails) userDetail).getAuthorities());
            return authorities;
        } else {
            return Collections.emptyList();
        }
    }

    /**
     * Parses the SAMLCredential for expiration time. Locates all AuthnStatements present within the assertion
     * (only one in most cases) and computes the expiration based on sessionNotOnOrAfter field.
     *
     * @param credential credential to use for expiration parsing.
     * @return null if no expiration is present, expiration time onOrAfter which the token is not valid anymore
     */
    protected Date getExpirationDate(SAMLCredential credential) {
        List<AuthnStatement> statementList = credential.getAuthenticationAssertion().getAuthnStatements();
        DateTime expiration = null;
        for (AuthnStatement statement : statementList) {
            DateTime newExpiration = statement.getSessionNotOnOrAfter();
            if (newExpiration != null) {
                if (expiration == null || expiration.isAfter(newExpiration)) {
                    expiration = newExpiration;
                }
            }
        }
        return expiration != null ? expiration.toDate() : null;
    }

    /**
     * Returns saml user details service used to load information about logged user from SAML data.
     *
     * @return service or null if not set
     */
    public SAMLUserDetailsService getUserDetails() {
        return userDetails;
    }

    /**
     * SAMLAuthenticationToken is the only supported token.
     *
     * @param aClass class to check for support
     * @return true if class is of type SAMLAuthenticationToken
     */
    public boolean supports(Class aClass) {
        return SAMLAuthenticationToken.class.isAssignableFrom(aClass);
    }

    /**
     * The user details can be optionally set and is automatically called while user SAML assertion
     * is validated.
     *
     * @param userDetails user details
     */
    @Autowired(required = false)
    public void setUserDetails(SAMLUserDetailsService userDetails) {
        this.userDetails = userDetails;
    }

    /**
     * Logger for SAML events, cannot be null, must be set.
     *
     * @param samlLogger logger
     */
    @Autowired
    public void setSamlLogger(SAMLLogger samlLogger) {
        Assert.notNull(samlLogger, "SAMLLogger can't be null");
        this.samlLogger = samlLogger;
    }

    /**
     * Profile for consumption of processed messages, must be set.
     *
     * @param consumer consumer
     */
    @Autowired
    @Qualifier("webSSOprofileConsumer")
    public void setConsumer(WebSSOProfileConsumer consumer) {
        Assert.notNull(consumer, "WebSSO Profile Consumer can't be null");
        this.consumer = consumer;
    }

    /**
     * Profile for consumption of processed messages using the Holder-of-Key profile, must be set.
     *
     * @param hokConsumer holder-of-key consumer
     */
    @Autowired
    @Qualifier("hokWebSSOprofileConsumer")
    public void setHokConsumer(WebSSOProfileConsumer hokConsumer) {
        this.hokConsumer = hokConsumer;
    }

    public boolean isForcePrincipalAsString() {
        return forcePrincipalAsString;
    }

    /**
     * By default principal in the returned Authentication object is the NameID included in the
     * authenticated Assertion. The NameID is not serializable.
     *
     * Setting this value to true will force the NameID value to be a String.
     *
     * @param forcePrincipalAsString true to force principal to be a String
     */
    public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
        this.forcePrincipalAsString = forcePrincipalAsString;
    }

    public boolean isExcludeCredential() {
        return excludeCredential;
    }

    /**
     * When false (default) the resulting Authentication object will include instance of SAMLCredential
     * as a credential value. The credential includes information related to the authentication
     * process, received attributes and is required for Single Logout.
     *
     * In case your application doesn't require the credential, it is possible to exclude it from
     * the Authentication object by setting this flag to true.
     *
     * @param excludeCredential false to include credential in the Authentication object, true to exclude it
     */
    public void setExcludeCredential(boolean excludeCredential) {
        this.excludeCredential = excludeCredential;
    }

    /**
     * Verifies that required entities were autowired or set.
     */
    public void afterPropertiesSet() throws ServletException {
        Assert.notNull(consumer, "WebSSO Profile Consumer can't be null");
        Assert.notNull(hokConsumer, "WebSSO Profile HoK Consumer can't be null");
        Assert.notNull(samlLogger, "SAMLLogger can't be null");
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是 Spring Boot 整合 SAML 的基本代码示例: 1. 添加依赖 在 pom.xml 文件中添加以下依赖: ```xml <dependency> <groupId>org.springframework.security.extensions</groupId> <artifactId>spring-security-saml2-core</artifactId> <version>1.0.10.RELEASE</version> </dependency> ``` 2. 配置 SAML 在 application.properties 文件中添加以下配置: ```properties # SAML 配置 security.saml2.metadata-url=http://idp.example.com/metadata security.saml2.entity-id=http://sp.example.com/metadata security.saml2.key-store=file:/path/to/keystore.jks security.saml2.key-store-password=keystore_password security.saml2.key-password=key_password security.saml2.default-success-url=/success security.saml2.login-processing-url=/saml/login security.saml2.logout-url=/saml/logout ``` 其中,metadata-url 是 Identity Provider 的元数据 URL,entity-id 是 Service Provider 的实体 ID,key-store 是密钥库文件路径,key-store-password 是密钥库密码,key-password 是密钥密码。 3. 配置 Spring Security 创建一个继承 WebSecurityConfigurerAdapter 的类,并在其中配置 Spring Security: ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SAMLUserDetailsService samlUserDetailsService; @Autowired private SAMLAuthenticationProvider samlAuthenticationProvider; @Autowired private SAMLConfigurer samlConfigurer; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/saml/**").permitAll() .anyRequest().authenticated() .and() .apply(samlConfigurer); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .authenticationProvider(samlAuthenticationProvider); } @Bean public SAMLConfigurer samlConfigurer() { return new SAMLConfigurer(); } @Bean public SAMLAuthenticationProvider samlAuthenticationProvider() { SAMLAuthenticationProvider provider = new SAMLAuthenticationProvider(); provider.setUserDetails(samlUserDetailsService); provider.setForcePrincipalAsString(false); return provider; } @Bean public SAMLUserDetailsService samlUserDetailsService() { return new SAMLUserDetailsServiceImpl(); } } ``` 其中,samlUserDetailsService 是一个实现 SAMLUserDetailsService 接口的类,用于加载用户信息。 4. 创建 SAML Controller 创建一个 SAML Controller,用于处理 SAML 相关请求: ```java @Controller @RequestMapping("/saml") public class SamlController { @GetMapping("/login") public void login(HttpServletRequest request, HttpServletResponse response) throws Exception { AuthenticationManager authenticationManager = getAuthenticationManager(); SAMLAuthenticationToken token = new SAMLAuthenticationToken(null, null); Authentication authentication = authenticationManager.authenticate(token); SecurityContextHolder.getContext().setAuthentication(authentication); response.sendRedirect("/"); } @GetMapping("/logout") public void logout(HttpServletRequest request, HttpServletResponse response) throws Exception { request.getSession().invalidate(); response.sendRedirect("/"); } private AuthenticationManager getAuthenticationManager() { AuthenticationManager authenticationManager = new ProviderManager(List.of(samlAuthenticationProvider())); return authenticationManager; } @Autowired private SAMLAuthenticationProvider samlAuthenticationProvider; } ``` 其中,login 方法用于处理 SAML 登录请求,logout 方法用于处理 SAML 登出请求。 5. 启动应用程序 启动应用程序,并访问 http://localhost:8080/saml/login 进行 SAML 登录。登录成功后,将重定向到 http://localhost:8080/success 页面。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值