package org.openxdm.xcap.server.slee;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.slee.ActivityContextInterface;
import javax.slee.ChildRelation;
import javax.slee.RolledBackContext;
import javax.slee.SbbContext;
import org.apache.log4j.Logger;
import org.mobicents.slee.enabler.userprofile.UserProfile;
import org.mobicents.slee.enabler.userprofile.UserProfileControlSbbLocalObject;
import org.mobicents.slee.xdm.server.ServerConfiguration;
import org.openxdm.xcap.common.error.InternalServerErrorException;
import org.openxdm.xcap.common.http.HttpConstant;
import org.openxdm.xcap.server.slee.auth.RFC2617AuthQopDigest;
import org.openxdm.xcap.server.slee.auth.RFC2617ChallengeParamGenerator;
/**
*
* @author aayush.bhatnagar
* @author martins
*
* From the OMA-TS-XDM-core specification:
*
* The Aggregation Proxy SHALL act as an HTTP Proxy defined in [RFC2616]
* with the following clarifications. The Aggregation Proxy:
*
* 1. SHALL be configured as an HTTP reverse proxy (see [RFC3040]);
*
* 2. SHALL support authenticating the XDM Client; in case the GAA is
* used according to [3GPP TS 33.222], the mutual authentication SHALL
* be supported; or SHALL assert the XDM Client identity by inserting
* the X-XCAPAsserted- Identity extension header to the HTTP requests
* after a successful HTTP Digest Authentication as defined in Section
* 6.3.2, in case the GAA is not used.
*
* 3. SHALL forward the XCAP requests to the corresponding XDM Server,
* and forward the response back to the XDM Client;
*
* 4. SHALL protect the XCAP traffic by enabling TLS transport security
* mechanism. The TLS resumption procedure SHALL be used as specified in
* [RFC2818].
*
* When realized with 3GPP IMS or 3GPP2 MMD networks, the Aggregation
* Proxy SHALL act as an Authentication Proxy defined in [3GPP TS
* 33.222] with the following clarifications. The Aggregation Proxy:
* SHALL check whether an XDM Client identity has been inserted in
* X-3GPP-Intended-Identity header of HTTP request.
*
* � If the X-3GPP-Intended-Identity is included , the Aggregation Proxy
* SHALL check the value in the header is allowed to be used by the
* authenticated identity.
*
* � If the X-3GPP-Intended-Identity is not included, the Aggregation
* Proxy SHALL insert the authenticated identity in the
* X-3GPP-Asserted-Identity header of the HTTP request.
*
* TODO: GAA is not supported as of now. It is FFS on how we go about
* GAA support. TODO: TLS is not supported as of now.
*/
public abstract class AuthenticationProxySbb implements javax.slee.Sbb,
AuthenticationProxySbbLocalObject {
private static final Logger logger = Logger
.getLogger(AuthenticationProxySbb.class);
private static final RFC2617ChallengeParamGenerator challengeParamGenerator = new RFC2617ChallengeParamGenerator();
private Context myEnv = null;
public String authenticationRealm = null;
/*
* (non-Javadoc)
*
* @see
* org.openxdm.xcap.server.slee.AuthenticationProxySbbLocalObject#authenticate
* (javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
public String authenticate(HttpServletRequest request,
HttpServletResponse response) throws InternalServerErrorException {
if (logger.isDebugEnabled()) {
logger.debug("Authenticating request");
}
/**
* On receiving an HTTP request that does not contain the Authorization
* header field, the AP shall: a) challenge the user by generating a 401
* Unauthorized response according to the procedures specified in TS 133
* 222 [6] and RFC 2617 [3]; and b) forward the 401 Unauthorized
* response to the sender of the HTTP request.
*/
try {
if (request.getHeader(HttpConstant.HEADER_AUTHORIZATION) == null) {
challengeRequest(request, response);
return null;
} else {
String user = checkAuthenticatedCredentials(request, response);
if (user != null) {
if (logger.isDebugEnabled()) {
logger.debug("Authentication suceed");
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("Authentication failed");
}
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().close();
}
return user;
}
} catch (Throwable e) {
throw new InternalServerErrorException(e.getMessage(), e);
}
}
/**
*
* @param request
* @param response
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws InternalServerErrorException
*/
private void challengeRequest(HttpServletRequest request,
HttpServletResponse response) throws IOException,
NoSuchAlgorithmException, InternalServerErrorException {
if (logger.isDebugEnabled())
logger
.debug("Authorization header is missing...challenging the request");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
/**
* If a qop directive is sent by the server in the challenge, then the
* challenge response MUST contain the nonce-count and cnonce
* parameters. This will be checked later on.
*/
String opaque = challengeParamGenerator.generateOpaque();
final String challengeParams = "Digest nonce=\"" + challengeParamGenerator.getNonce(opaque)
+ "\", realm=\"" + getRealm()
+ "\", opaque=\"" + opaque
+ "\", qop=auth";
response.setHeader(HttpConstant.HEADER_WWW_AUTHENTICATE,
challengeParams);
if (logger.isDebugEnabled()) {
logger.debug("Sending response with header "+HttpConstant.HEADER_WWW_AUTHENTICATE+" challenge params: "+challengeParams);
}
// send to client
response.getWriter().close();
}
/**
*
* @param request
* @param response
* @return null if authentication failed, authenticated user@domain otherwise
* @throws InternalServerErrorException
*/
private String checkAuthenticatedCredentials(HttpServletRequest request,
HttpServletResponse response) throws InternalServerErrorException {
/**
* On receiving an HTTP request that contains the Authorization header
* field, the AP shall:
*
* a)use the value of that username parameter of the Authorization
* header field to authenticate the user;
*
* b)apply the procedures specified in RFC 2617 [3] for authentication;
*
* c)if the HTTP request contains an X 3GPP Intended Identity header
* field (TS 124 109 [5]), then the AP may verify that the user identity
* belongs to the subscriber. This verification of the user identity
* shall be performed dependant on the subscriber's application specific
* or AP specific user security settings;
*
* d)if authentication is successful, remove the Authorization header
* field from the HTTP request;
*
* e)insert an HTTP X 3GPP Asserted Identity header field (TS 124 109
* [5]) that contains the asserted identity or a list of identities;
*
* We wont be implementing points d and e, as they are applicable only
* if the Authentication Proxy had to forward the request to the XDM
* over the network. Here it is co-located with the XDM server.
*/
String authHeaderParams = request
.getHeader(HttpConstant.HEADER_AUTHORIZATION);
if (logger.isDebugEnabled()) {
logger.debug("Authorization header included with value: "+authHeaderParams);
}
// 6 is "Digest".length(), lets skip the header value till that index
final int digestParamsStart = 6;
if (authHeaderParams.length() > digestParamsStart) {
authHeaderParams = authHeaderParams.substring(digestParamsStart);
}
String username = null;
String password = null;
String realm = null;
String nonce = null;
String uri = null;
String cnonce = null;
String nc = null;
String qop = null;
String resp = null;
String opaque = null;
for(String param : authHeaderParams.split(",")) {
int i = param.indexOf('=');
if (i > 0 && i < (param.length()-1)) {
String paramName = param.substring(0,i).trim();
String paramValue = param.substring(i+1).trim();
if (paramName.equals("username")) {
if (paramValue.length()>2) {
username = paramValue.substring(1, paramValue.length()-1);
if (logger.isDebugEnabled()) {
logger.debug("Username param with value "+username);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid param "+paramName+" value "+paramValue);
}
}
}
else if (paramName.equals("nonce")) {
if (paramValue.length()>2) {
nonce = paramValue.substring(1, paramValue.length()-1);
if (logger.isDebugEnabled()) {
logger.debug("Nonce param with value "+nonce);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid param "+paramName+" value "+paramValue);
}
}
}
else if (paramName.equals("cnonce")) {
if (paramValue.length()>2) {
cnonce = paramValue.substring(1, paramValue.length()-1);
if (logger.isDebugEnabled()) {
logger.debug("CNonce param with value "+cnonce);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid param "+paramName+" value "+paramValue);
}
}
}
else if (paramName.equals("realm")) {
if (paramValue.length()>2) {
realm = paramValue.substring(1, paramValue.length()-1);
if (logger.isDebugEnabled()) {
logger.debug("Realm param with value "+realm);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid param "+paramName+" value "+paramValue);
}
}
}
else if (paramName.equals("nc")) {
nc = paramValue;
if (logger.isDebugEnabled()) {
logger.debug("Nonce-count param with value "+nc);
}
}
else if (paramName.equals("response")) {
if (paramValue.length()>2) {
resp = paramValue.substring(1, paramValue.length()-1);
if (logger.isDebugEnabled()) {
logger.debug("Response param with value "+resp);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid param "+paramName+" value "+paramValue);
}
}
}
else if (paramName.equals("uri")) {
if (paramValue.length()>2) {
uri = paramValue.substring(1, paramValue.length()-1);
if (logger.isDebugEnabled()) {
logger.debug("Digest uri param with value "+uri);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid param "+paramName+" value "+paramValue);
}
}
}
else if (paramName.equals("opaque")) {
if (paramValue.length()>2) {
opaque = paramValue.substring(1, paramValue.length()-1);
if (logger.isDebugEnabled()) {
logger.debug("Opaque param with value "+opaque);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid param "+paramName+" value "+paramValue);
}
}
}
else if (paramName.equals("qop")) {
if (paramValue.charAt(0) == '"') {
if (paramValue.length()>2) {
qop = paramValue.substring(1, paramValue.length()-1);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid param "+paramName+" value "+paramValue);
}
}
}
else {
qop = paramValue;
}
if (logger.isDebugEnabled()) {
logger.debug("Qop param with value "+qop);
}
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid param "+param);
}
}
}
/**
* The client response to a WWW-Authenticate challenge for a protection
* space starts an authentication session with that protection space.
* The authentication session lasts until the client receives another
* WWW-Authenticate challenge from any server in the protection space. A
* client should remember the username, password, nonce, nonce count and
* opaque values associated with an authentication session to use to
* construct the Authorization header in future requests within that
* protection space.
*/
if (username == null || realm == null || nonce == null || cnonce == null || nc == null
|| uri == null || resp == null || opaque == null) {
logger
.error("A required parameter is missing in the challenge response");
// FIXME should be replied with BAD REQUEST 400
return null;
}
// verify opaque vs nonce
if (challengeParamGenerator.getNonce(opaque).equals(nonce)) {
if (logger.isDebugEnabled())
logger.debug("Nonce provided matches the one generated using opaque as seed");
}
else {
if (logger.isDebugEnabled())
logger.debug("Authentication failed, nonce provided doesn't match the one generated using opaque as seed");
return null;
}
if (!qop.equals("auth")) {
if (logger.isDebugEnabled())
logger.debug("Authentication failed, qop value "+qop+" unsupported");
return null;
}
// get user password
UserProfile userProfile = getUserProfileControlSbb().find(username);
if (userProfile == null) {
if (logger.isDebugEnabled())
logger.debug("Authentication failed, profile not found for user "+username);
return null;
}
else {
password = userProfile.getPassword();
}
final String digest = new RFC2617AuthQopDigest(username, realm, password, nonce, nc, cnonce, request.getMethod().toUpperCase(), uri).digest();
if (digest != null && digest.equals(resp)) {
if (logger.isDebugEnabled())
logger.debug("authentication response is matching");
/**
* Add the cnonce,nc and qop as received in the Authorization header
* of the request. We need to add the Authentication-Info header and
* set these values.
*
* Authentication-Info: qop=auth-int,
* rspauth="6629fae49394a05397450978507c4ef1",
* cnonce="6629fae49393a05397450978507c4ef1", nc=00000001
*/
String params = "cnonce=\"" + cnonce + "\", nc=" + nc + ", qop="
+ qop + ", rspauth=\"" + digest+"\"";
response.addHeader("Authentication-Info", params);
return username;
} else {
if (logger.isDebugEnabled())
logger.debug("authentication response digest received ("+resp+") didn't match the one calculated ("+digest+")");
return null;
}
}
/**
* Get the authentication scheme
*
* @return the scheme name
*/
public String getScheme() {
return "Digest";
}
/**
* get the authentication realm
*
* @return the realm name
*/
public String getRealm() {
return this.authenticationRealm;
}
// -- user profile enabler child relation
public abstract ChildRelation getUserProfileControlChildRelation();
protected UserProfileControlSbbLocalObject getUserProfileControlSbb() {
try {
return (UserProfileControlSbbLocalObject) getUserProfileControlChildRelation()
.create();
} catch (Exception e) {
logger.error("Failed to create child sbb", e);
return null;
}
}
// -- sbb object lifecycle
public void setSbbContext(SbbContext context) {
if (logger.isDebugEnabled())
logger.debug("Set sbb context for AuthenticationProxy Sbb");
this.sbbContext = context;
try {
myEnv = (Context) new InitialContext().lookup("java:comp/env");
this.authenticationRealm = (String) myEnv
.lookup("authenticationRealm");
if (authenticationRealm.equals("")) {
authenticationRealm = ServerConfiguration.SERVER_HOST;
}
} catch (NamingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void unsetSbbContext() {
this.sbbContext = null;
}
public void sbbCreate() throws javax.slee.CreateException {
}
public void sbbPostCreate() throws javax.slee.CreateException {
}
public void sbbActivate() {
}
public void sbbPassivate() {
}
public void sbbRemove() {
}
public void sbbLoad() {
}
public void sbbStore() {
}
public void sbbExceptionThrown(Exception exception, Object event,
ActivityContextInterface activity) {
}
public void sbbRolledBack(RolledBackContext context) {
}
protected SbbContext getSbbContext() {
return sbbContext;
}
private SbbContext sbbContext; // This SBB's SbbContext
}