Red5 Authentication
May 7th, 2012 by Paul Gregoire
How to implement CRAM authentication in Red5
In this post we will setup a challenge-response authentication mechanism (CRAM) in a Red5 application using two different methods; the first one being very simple and the other utilizing the powerful Spring security libraries. A basic challenge-response process works like so:
- Client requests a session
- Server generates a unique, random ChallengeString (e.g. salt, guid) as well as a SessionID and sends both to client
- 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
- Client sends the server UserID, ResponseString and SessionID
- 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.
Before we proceed further, it is assumed that you are somewhat familiar with Red5 applications and the Flex SDK. For those who would like a quick set of screencasts to get up-to-speed, we offer the following:
Implementation
Implementing a security mechanism is as simple as adding an application lifecycle listener to your application. Red5 supports a couple types of CRAM authentication via an available auth plugin. The first one implements the FMS authentication routine and the other one is a custom routine developed by the Red5 team. In this post we will use the Red5 routine. An ApplicationLifecycle class implementing the Red5 routine, may be found in the Red5 source repository; this code only validates against the password “test”. While this class would not be useful in production, it may certainly be used as a starting point for a real implementation. Red5AuthenticationHandler Source
To enable the usage of your Red5AuthenticationHandler class or any other ApplicationLifecycle type class for that matter, you must add it to the listeners in your Application’s appStart method.
1 2 3 4 5 6 7 |
@Override public boolean appStart(IScope scope) { addListener((IApplication) applicationContext.getBean("authHandler")); return super.appStart(scope); } |
The reason for putting it in the appStart method is to ensure that the handler is added when your application starts and before it starts accepting connections. There is no other code to add to your application adapter at this point since the lifecycle methods will fire in your handler. Putting the authentication code within a lifecycle handler serves to keep the adapter code cleaner and less confusing. The authentication handler is defined in the red5-web.xml like so:
1 2 3 |
<bean id="authHandler" class="org.red5.demo.auth.Red5AuthenticationHandler" /> |
At this point, your application would require authentication before a connection would be allowed to proceed beyond “connect”. Entering any user name and the password of “test” or “password” (depends on class used in demo) would allow a client to be connected. As stated previously, this first “simple” implementation is not meant for production but is offered as a means to understand the mechanism at work.
Adding Spring Security
Once we add security layers such as Spring security, the application and authentication features become much more robust. The first thing we must do is to add the Spring Security namespace to our applications red5-web.xml.
Replace this node:
1 2 3 4 5 6 7 |
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:lang="http://www.springframework.org/schema/lang" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-3.0.xsd"> |
With this node:
1 2 3 4 5 6 7 |
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:security="http://www.springframework.org/schema/security" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.3.xsd> |
Add the authentication manager bean and configure it to use a plain text file. The users file contains our users, passwords, and their credentials.
1 2 3 4 5 6 7 |
<security:authentication-manager alias="authManager"> <security:authentication-provider> <security:user-service properties="WEB-INF/classes/users.properties" /> </security:authentication-provider> </security:authentication-manager> |
To demonstrate how users are handled, we will create three users: 1 admin, 1 regular user, and 1 user without a role. The plain text users file follows this pattern: username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] A user can have more than one role specified; granted authority and role are synonymous. Below are the contents of our users file for this demo.
1 2 3 4 5 |
admin=password,ROLE_USER,ROLE_ADMIN,enabled user1=pass1,ROLE_USER,enabled user2=pass2,ROLE_NONE,enabled |
In addition to the authentication handler, a authentication manager must be added when using Spring security. The manager should be added to the appStart method prior to adding the handler, as shown below.
1 2 3 4 5 6 7 8 9 10 11 |
@Override public boolean appStart(IScope scope) { ProviderManager authManager = (ProviderManager) applicationContext.getBean("authManager"); if (authManager.isEraseCredentialsAfterAuthentication()) { authManager.setEraseCredentialsAfterAuthentication(false); } addListener((IApplication) applicationContext.getBean("authHandler")); return super.appStart(scope); } |
The Spring security version of the authentication handler will replace the simple version in your red5-web.xml like so:
1 2 3 |
<bean id="authHandler" class="org.red5.demo.auth.Red5SpringAuthenticationHandler" /> |
Lastly, an aggregated user details service is used for storage and look ups of user details; this is essentially an interface to the internal in-memory datastore holding the user details or credentials. The user details may be configured to retrieve details from our local properties file, databases, ldap, or active directory. Our aggregated service is fairly simple as you can see below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { UserDetails userDetails = null; Map<String, UserDetailsService> userDetailsServiceMap = applicationContext.getBeansOfType(UserDetailsService.class); for (Entry<String, UserDetailsService> entry : userDetailsServiceMap.entrySet()) { UserDetailsService val = entry.getValue(); if (val instanceof AggregatedUserDetailsService) { continue; } UserDetails tmp = val.loadUserByUsername(userName); if (tmp != null) { userDetails = tmp; break; } } if (userDetails == null) { throw new UsernameNotFoundException("User " + userName + " not found"); } return userDetails; } |
It should be noted that Spring security makes use of an additional Spring framework library that is not included in Red5; the transaction library provides DAO and transaction implementations which do not require an external database or related dependencies. All the libraries required for the demo are included in the project zip file.
Client code
Creation of an authentication enabled client will require a single library not included in Flex / Flash builder called as3crypto. The AS3 cryptography library will provide the hashing functions nessasary for authentication in our demo. Download the as3crypto.swc from: http://code.google.com/p/as3crypto/ and place it in the “libs” folder of our client project.
The following functions will be needed in your client support authentication:
1 2 3 4 5 |
sendCredentials(evt:Event):void sendCreds():void computeSimpleSHA256(text:String):String computeHMACSHA256(key:String, input:String):String onStatus(evt:NetStatusEvent):void |
The sendCreds method is called “later” from the sendCredentials method to prevent issues in the event thread.
These are the imports that need to be added before beginning.
1 2 3 4 5 6 7 8 9 10 11 12 |
import com.hurlant.crypto.Crypto; import com.hurlant.crypto.hash.HMAC; import com.hurlant.crypto.hash.IHash; import com.hurlant.util.Base64; import com.hurlant.util.Hex; import flash.events.*; import flash.media.*; import flash.net.*; import flash.utils.ByteArray; import mx.utils.SHA256; |
In your connect function you will need to determine which authentication mode to employ. The following block will show how to set up the connect call based on the type selected.
1 2 3 4 5 6 7 8 9 |
if (authMode === "none") { nc.connect(serverUrl.text, null); } else if (authMode === "red5") { nc.connect(serverUrl.text + "?authmod=red5&user=" + user.text); } else if (authMode === "adobe") { nc.connect(serverUrl.text + "?authmod=adobe&user=" + user.text); } |
You may notice that the type is simply a string in the url denoting which mode to use.
In your net status event handler, you will need to add handling for authentication routines. The following block demonstrates how to do so when an NetConnection.Connect.Rejected is received.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public function onStatus(evt:NetStatusEvent):void { if (evt.info !== '' || evt.info !== null) { var desc:String = evt.info.description; switch (evt.info.code) { case "NetConnection.Connect.Rejected": if (desc !== '') { if (desc.indexOf("reason=badauth") > 0) { // auth failed due to user/pass } else if (desc.indexOf("code=403") > 0) { // authentication is required var tmp:String = String(evt.info.description); tmp = tmp.slice(tmp.indexOf("authmod") + 8, tmp.indexOf("]") - 1); } else { try { var parameters:Object = {}; var params:Array = desc.split('?')[1].split('&'); var length:int = params.length; for (var i:int = 0, index:int = -1;i 0) { var key : String = kvPair.substring(0, index); var value : String = kvPair.substring(index + 1); parameters[key] = value; } } if (parameters["reason"] == "needauth") { challenge = parameters["challenge"]; sessionId = parameters["sessionid"]; // send the credentials sendCredentials(null); } } catch(e:Error) { } } } break; } } } |
Once you are successfully connected, there are two methods in the demo code for testing access. The helloAdmin method will return a positive result if the authenticated user has the admin permission. In the helloUser method the routine is similar, but only the user permission is required. The included users file contains an admin user and two regular users, the second user is set up to have no permissions. The user without permissions may only connect application and call unprotected methods.
Protected methods in the application contain an isAuthorized check against preselected permissions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public static boolean isAuthorized(String... roles) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated()) { UserDetails deets = (UserDetails) auth.getPrincipal(); Collection granted = deets.getAuthorities(); for (GrantedAuthority authority : granted) { if (Arrays.asList(roles).contains(authority.getAuthority())) { return true; } } } return false; } public String helloAdmin() { if (isAuthorized("ROLE_ADMIN")) { return "Hello Admin!"; } return "You are not authorized"; } |
If the user does not qualify, the call fails.
In a future post, I will explain how to add Java Authentication and Authorization Service (JAAS) to Red5.
Download
Project Source Client and server
- 5 Comments »
- Posted in Flex, Red5, Tutorials