org.red5.webapps.admin.handler.Red5AuthenticationHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.red5.webapps.admin.handler.Red5AuthenticationHandler.java

Source

/*
 * RED5 Open Source Flash Server - http://code.google.com/p/red5/
 * 
 * Copyright 2006-2012 by respective authors (see below). All rights reserved.
 * 
 * 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
 * 
 * http://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.red5.webapps.admin.handler;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.HashMap;
import java.util.Map;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.Arrays;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.adapter.ApplicationLifecycle;
import org.red5.server.api.IConnection;
import org.red5.server.exception.ClientRejectedException;
import org.red5.server.net.rtmp.RTMPConnection;
import org.red5.server.session.SessionManager;
import org.red5.webapps.admin.controllers.service.AggregatedUserDetailsService;
import org.red5.webapps.admin.utils.UrlQueryStringMap;
import org.slf4j.Logger;
import org.springframework.context.ApplicationContext;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

/**
 * Provides Red5 specific authentication using an application listener.
 * 
 * This handler uses a basic challenge-response protocol:
<ul>  
<li>Client requests a session</li>  
<li>Server generates a unique, random ChallengeString (e.g. salt, guid) as well as a SessionID and sends both to client</li>  
<li>Client gets UserID and Password from UI. Hashes the password once and call it PasswordHash.   Then combines PasswordHash with the 
random string received from server in step 2, and hashes them together again, call this ResponseString</li>  
<li>Client sends the server UserID, ResponseString and SessionID</li>  
<li>Server looks up users stored PasswordHash based on UserID, and the original ChallengeString based on SessionID.   Then computes the
ResponseHash by hashing the PasswordHash and ChallengeString.   If its equal to the ResponseString sent by user, then authentication succeeds.</li>  
</ul>
 * 
 * @author Paul Gregoire
 */
public class Red5AuthenticationHandler extends ApplicationLifecycle {

    private static Logger log = Red5LoggerFactory.getLogger(Red5AuthenticationHandler.class, "enterprise-server");

    private ApplicationContext applicationContext;

    private static String rejectMissingAuth = "[ code=403 .need auth; authmod=red5 ]";

    private static String invalidAuthMod = "[ AccessManager.Reject ] : [ authmod=red5 ] : ?reason=invalid_authmod";

    private static String badAuth = "[ AccessManager.Reject ] : [ authmod=red5 ] : ?reason=badauth";

    private static String noSuchUser = "[ AccessManager.Reject ] : [ authmod=red5 ] : ?reason=nosuchuser";

    //private static String invalidSessionId = "[ AccessManager.Reject ] : [ authmod=red5 ] : ?reason=invalid_session_id";

    private Mac hmacSHA256;

    //salt to use for challenge string generation
    private String salt = "red5adminapp5150";

    //map of challenge strings, keyed by session id
    private Map<String, String> sessionChallenges = new HashMap<String, String>();

    static {
        //get security provider
        Security.addProvider(new BouncyCastleProvider());
    }

    {
        try {
            hmacSHA256 = Mac.getInstance("HmacSHA256");
        } catch (SecurityException e) {
            log.error("Security exception when getting HMAC", e);
        } catch (NoSuchAlgorithmException e) {
            log.error("HMAC SHA256 does not exist");
        }
    }

    public Red5AuthenticationHandler(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public boolean appConnect(IConnection conn, Object[] params) {
        log.info("appConnect");
        // start with negative result
        boolean result = false;
        log.debug("Connection: {}", conn);
        log.debug("Params: {}", params);
        // start off with the status being bad authentication
        String status = badAuth;
        // get the connection parameters
        Map<String, Object> connectionParams = conn.getConnectParams();
        log.debug("Connection params: {}", connectionParams);
        if (!connectionParams.containsKey("queryString")) {
            //set as missing auth notification
            status = rejectMissingAuth;
        } else {
            //get the raw query string
            String rawQueryString = (String) connectionParams.get("queryString");
            try {
                //parse into a usable query string
                UrlQueryStringMap<String, String> queryString = UrlQueryStringMap.parse(rawQueryString);
                log.debug("Query string: {}", queryString);
                //get the values we want
                String userName = queryString.get("user");
                log.debug("User: {}", userName);
                // do a user lookup
                AggregatedUserDetailsService userDetailsService = (AggregatedUserDetailsService) applicationContext
                        .getBean("aggregatedUserDetailsService");
                // this will throw an exception if the user cant be located by name
                UserDetails userDetails = userDetailsService.loadUserByUsername(userName);
                // get the authentication "style"
                String authmod = queryString.get("authmod");
                log.debug("Authmod: {}", authmod);
                //make sure they requested red5 auth
                if ("red5".equals(authmod)) {
                    String response = queryString.get("response");
                    if (response != null) {
                        response = queryString.get("response").replace(' ', '+');
                    }
                    log.debug("Response: {}", response);
                    //try the querystring first
                    String sessionId = queryString.get("sessionid");
                    if (sessionId == null) {
                        //get the session id - try conn next
                        sessionId = ((RTMPConnection) conn).getSessionId();
                        if (sessionId == null) {
                            //use attribute
                            if (conn.hasAttribute("sessionId")) {
                                sessionId = conn.getStringAttribute("sessionId");
                            } else {
                                sessionId = SessionManager.getSessionId();
                                conn.setAttribute("sessionId", sessionId);
                            }
                        }
                    }
                    log.debug("Session id: {}", sessionId);
                    String challenge = null;
                    if (response != null) {
                        //look up challenge (gets and removes at the same time)
                        challenge = sessionChallenges.remove(sessionId);
                        // get the password
                        String password = userDetails.getPassword();
                        log.debug("Users password: {}", password);
                        //generate response hash to compare
                        String responseHash = calculateHMACSHA256(challenge, password);
                        log.debug("Generated response: {}", responseHash);
                        log.debug("Generated response: {}", response);
                        //decode both hashes before we compare otherwise we will have issues like
                        //4+5WioxdBLhx4qajIybxkBkynDsv7KxtNzqj4V/VbzU != 4+5WioxdBLhx4qajIybxkBkynDsv7KxtNzqj4V/VbzU=                    
                        if (Arrays.areEqual(Base64.decodeBase64(responseHash.getBytes()),
                                Base64.decodeBase64(response.getBytes()))) {
                            // everything matches so now do the actual authentication
                            // get the authentication manager
                            ProviderManager authManager = (ProviderManager) applicationContext
                                    .getBean("authenticationManager");
                            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                                    userName, password);
                            Authentication auth = null;
                            try {
                                auth = authManager.authenticate(token);
                                log.info("Authentication result: {}\ndetails: {}", auth.isAuthenticated(), auth);
                                result = auth.isAuthenticated();
                                // set the authenticated user into the context (thread-local)
                                if (result) {
                                    SecurityContextHolder.getContext().setAuthentication(auth);
                                }
                            } catch (Exception ex) {
                                log.warn("Problem during auth attempt: {}", ex);
                            }
                        }
                    } else if (authmod != null && userName != null) {
                        // generate a challenge
                        challenge = calculateHMACSHA256(salt, sessionId);
                        // store the generated data
                        sessionChallenges.put(sessionId, challenge);
                        // set as rejected
                        status = String.format(
                                "[ AccessManager.Reject ] : [ authmod=red5 ] : ?reason=needauth&user=%s&sessionid=%s&challenge=%s",
                                userName, sessionId, challenge);
                    }
                    log.debug("Challenge: {}", challenge);
                } else {
                    status = invalidAuthMod;
                }
            } catch (UsernameNotFoundException ex) {
                status = noSuchUser;
            } catch (Exception e) {
                log.error("Error authenticating", e);
            }
        }
        //send the status object
        log.debug("Status: {}", status);
        if (!result) {
            throw new ClientRejectedException(status);
        }
        return result;
    }

    /**
     * Generate an HMAC-SHA256 hash and return encoded with Base64.
     * 
     * @param key
     * @param input
     * @return
     */
    private String calculateHMACSHA256(String key, String input) {
        byte[] output = null;
        try {
            hmacSHA256.init(new SecretKeySpec(key.getBytes(), "HmacSHA256"));
            output = hmacSHA256.doFinal(input.getBytes());
        } catch (InvalidKeyException e) {
            log.error("Invalid key", e);
        }
        byte[] res = Base64.encodeBase64(output);
        String result = new String(res);
        //strip any cr/lf
        return result.replaceAll("(\r\n|\r|\n|\n\r)", "");
    }

    public String getSalt() {
        return salt;
    }

    public void setSalt(String salt) {
        this.salt = salt;
    }

}