Removing the Jersey Http Security Module plus the AkkaRestServlet
This commit is contained in:
parent
29f515fd20
commit
c3ee1244be
6 changed files with 0 additions and 989 deletions
|
|
@ -1,266 +0,0 @@
|
|||
HTTP Security
|
||||
=============
|
||||
|
||||
.. sidebar:: Contents
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
Module stability: **IN PROGRESS**
|
||||
|
||||
Akka supports security for access to RESTful Actors through `HTTP Authentication <http://en.wikipedia.org/wiki/HTTP_Authentication>`_. The security is implemented as a jersey ResourceFilter which delegates the actual authentication to an authentication actor.
|
||||
|
||||
Akka provides authentication via the following authentication schemes:
|
||||
|
||||
* `Basic Authentication <http://en.wikipedia.org/wiki/Basic_access_authentication>`_
|
||||
* `Digest Authentication <http://en.wikipedia.org/wiki/Digest_access_authentication>`_
|
||||
* `Kerberos SPNEGO Authentication <http://en.wikipedia.org/wiki/SPNEGO>`_
|
||||
|
||||
The authentication is performed by implementations of akka.security.AuthenticationActor.
|
||||
|
||||
Akka provides a trait for each authentication scheme:
|
||||
|
||||
* BasicAuthenticationActor
|
||||
* DigestAuthenticationActor
|
||||
* SpnegoAuthenticationActor
|
||||
|
||||
|
||||
Setup
|
||||
-----
|
||||
|
||||
To secure your RESTful actors you need to perform the following steps:
|
||||
|
||||
1. configure the resource filter factory 'akka.security.AkkaSecurityFilterFactory' in the 'akka.conf' like this:
|
||||
|
||||
.. code-block:: ruby
|
||||
|
||||
akka {
|
||||
...
|
||||
rest {
|
||||
filters="akka.security.AkkaSecurityFilterFactory"
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
2. Configure an implementation of an authentication actor in 'akka.conf':
|
||||
|
||||
.. code-block:: ruby
|
||||
|
||||
akka {
|
||||
...
|
||||
rest {
|
||||
filters= ...
|
||||
authenticator = "akka.security.samples.BasicAuthenticationService"
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
3. Start your authentication actor in your 'Boot' class. The security package consists of the following parts:
|
||||
|
||||
4. Secure your RESTful actors using class or resource level annotations:
|
||||
|
||||
* @DenyAll
|
||||
* @RolesAllowed(listOfRoles)
|
||||
* @PermitAll
|
||||
|
||||
Security Samples
|
||||
----------------
|
||||
|
||||
The akka-samples-security module contains a small sample application with sample implementations for each authentication scheme.
|
||||
You can start the sample app using the jetty plugin: mvn jetty:run.
|
||||
|
||||
The RESTful actor can then be accessed using your browser of choice under:
|
||||
|
||||
* permit access only to users having the “chef” role: `<http://localhost:8080//secureticker/chef>`_
|
||||
* public access: `<http://localhost:8080//secureticker/public>`_
|
||||
|
||||
You can access the secured resource using any user for basic authentication (which is the default authenticator in the sample app).
|
||||
|
||||
Digest authentication can be directly enabled in the sample app. Kerberos/SPNEGO authentication is a bit more involved an is described below.
|
||||
|
||||
|
||||
Kerberos/SPNEGO Authentication
|
||||
------------------------------
|
||||
|
||||
Kerberos is a network authentication protocol, (see `<http://www.ietf.org/rfc/rfc1510.txt>`_). It provides strong authentication for client/server applications.
|
||||
In a kerberos enabled environment a user will need to sign on only once. Subsequent authentication to applications is handled transparently by kerberos.
|
||||
|
||||
Most prominently the kerberos protocol is used to authenticate users in a windows network. When deploying web applications to a corporate intranet an important feature will be to support the single sign on (SSO), which comes to make the application kerberos aware.
|
||||
|
||||
How does it work (at least for REST actors)?
|
||||
|
||||
- When accessing a secured resource the server will check the request for the *Authorization* header as with basic or digest authentication.
|
||||
- If it is not set, the server will respond with a challenge to "Negotiate". The negotiation is in fact the NEGO part of the `SPNEGO <http://tools.ietf.org/html/rfc4178>`_ specification
|
||||
- The browser will then try to acquire a so called *service ticket* from a ticket granting service, i.e. the kerberos server
|
||||
- The browser will send the *service ticket* to the web application encoded in the header value of the *Authorization* header
|
||||
- The web application must validate the ticket based on a shared secret between the web application and the kerberos server. As a result the web application will know the name of the user
|
||||
|
||||
To activate the kerberos/SPNEGO authentication for your REST actor you need to enable the kerberos/SPNEGOauthentication actor in the akka.conf like this:
|
||||
|
||||
.. code-block:: ruby
|
||||
|
||||
akka {
|
||||
...
|
||||
rest {
|
||||
filters= ...
|
||||
authenticator = "akka.security.samples.SpnegoAuthenticationService"
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
Furthermore you must provide the SpnegoAuthenticator with the following information.
|
||||
|
||||
- Service principal name: the name of your web application in the kerberos servers user database. This name is always has the form ``HTTP/{server}@{realm}``
|
||||
- Path to the keytab file: this is a kind of certificate for your web application to acquire tickets from the kerberos server
|
||||
|
||||
.. code-block:: ruby
|
||||
|
||||
akka {
|
||||
...
|
||||
rest {
|
||||
filters= ...
|
||||
authenticator = "akka.security.samples.SpnegoAuthenticationService"
|
||||
kerberos {
|
||||
servicePrincipal = "HTTP/{server}@{realm}"
|
||||
keyTabLocation = "URL to keytab"
|
||||
# kerberosDebug = "true"
|
||||
}
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
How to setup kerberos on localhost for Ubuntu
|
||||
---------------------------------------------
|
||||
|
||||
This is a short step by step description of howto set up a kerberos server on an ubuntu system.
|
||||
|
||||
1. Install the Heimdal Kerberos Server and Client
|
||||
|
||||
::
|
||||
|
||||
sudo apt-get install heimdal-clients heimdal-clients-x heimdal-kdc krb5-config
|
||||
...
|
||||
|
||||
2. Set up your kerberos realm. In this example the realm is of course … EXAMPLE.COM
|
||||
|
||||
::
|
||||
|
||||
eckart@dilbert:~$ sudo kadmin -l
|
||||
kadmin> init EXAMPLE.COM
|
||||
Realm max ticket life [unlimited]:
|
||||
Realm max renewable ticket life [unlimited]:
|
||||
kadmin> quit
|
||||
|
||||
3. Tell your kerberos clients what your realm is and where to find the kerberos server (aka the Key Distribution Centre or KDC)
|
||||
|
||||
Edit the kerberos config file: /etc/krb5.conf and configure …
|
||||
…the default realm:
|
||||
|
||||
::
|
||||
|
||||
[libdefaults]
|
||||
default_realm = EXAMPLE.COM
|
||||
|
||||
… where to find the KDC for your realm
|
||||
|
||||
::
|
||||
|
||||
[realms]
|
||||
EXAMPLE.COM = {
|
||||
kdc = localhost
|
||||
}
|
||||
|
||||
…which hostnames or domains map to which realm (a kerberos realm is **not** a DNS domain):
|
||||
|
||||
::
|
||||
|
||||
[domain_realm]
|
||||
localhost = EXAMPLE.COM
|
||||
|
||||
4. Add the principals
|
||||
The user principal:
|
||||
|
||||
::
|
||||
|
||||
eckart@dilbert:~$ sudo kadmin -l
|
||||
kadmin> add zaphod
|
||||
Max ticket life [1 day]:
|
||||
Max renewable life [1 week]:
|
||||
Principal expiration time [never]:
|
||||
Password expiration time [never]:
|
||||
Attributes []:
|
||||
zaphod@EXAMPLE.COM's Password:
|
||||
Verifying - zaphod@EXAMPLE.COM's Password:
|
||||
kadmin> quit
|
||||
|
||||
The service principal:
|
||||
|
||||
::
|
||||
|
||||
eckart@dilbert:~$ sudo kadmin -l
|
||||
kadmin> add HTTP/localhost@EXAMPLE.COM
|
||||
Max ticket life [1 day]:
|
||||
Max renewable life [1 week]:
|
||||
Principal expiration time [never]:
|
||||
Password expiration time [never]:
|
||||
Attributes []:
|
||||
HTTP/localhost@EXAMPLE.COM's Password:
|
||||
Verifying - HTTP/localhost@EXAMPLE.COM's Password:
|
||||
kadmin> quit
|
||||
|
||||
We can now try to acquire initial tickets for the principals to see if everything worked.
|
||||
|
||||
::
|
||||
|
||||
eckart@dilbert:~$ kinit zaphod
|
||||
zaphod@EXAMPLE.COM's Password:
|
||||
|
||||
If this method returns withour error we have a success.
|
||||
We can additionally list the acquired tickets:
|
||||
|
||||
::
|
||||
|
||||
eckart@dilbert:~$ klist
|
||||
Credentials cache: FILE:/tmp/krb5cc_1000
|
||||
Principal: zaphod@EXAMPLE.COM
|
||||
|
||||
Issued Expires Principal
|
||||
Oct 24 21:51:59 Oct 25 06:51:59 krbtgt/EXAMPLE.COM@EXAMPLE.COM
|
||||
|
||||
This seems correct. To remove the ticket cache simply type kdestroy.
|
||||
|
||||
5. Create a keytab for your service principal
|
||||
|
||||
::
|
||||
|
||||
eckart@dilbert:~$ ktutil -k http.keytab add -p HTTP/localhost@EXAMPLE.COM -V 1 -e aes256-cts-hmac-sha1-96
|
||||
Password:
|
||||
Verifying - Password:
|
||||
eckart@dilbert:~$
|
||||
|
||||
This command will create a keytab file for the service principal named ``http.keytab`` in the current directory. You can specify other encryption methods than ‘aes256-cts-hmac-sha1-96’, but this is the e default encryption method for the heimdal client, so there is no additional configuration needed. You can specify other encryption types in the krb5.conf.
|
||||
|
||||
Note that you might need to install the unlimited strength policy files for java from here: `<http://java.sun.com/javase/downloads/index_jdk5.jsp>`_ to use the aes256 encryption from your application.
|
||||
|
||||
Again we can test if the keytab generation worked with the kinit command:
|
||||
|
||||
::
|
||||
|
||||
eckart@dilbert:~$ kinit -t http.keytab HTTP/localhost@EXAMPLE.COM
|
||||
eckart@dilbert:~$ klist
|
||||
Credentials cache: FILE:/tmp/krb5cc_1000
|
||||
Principal: HTTP/localhost@EXAMPLE.COM
|
||||
|
||||
Issued Expires Principal
|
||||
Oct 24 21:59:20 Oct 25 06:59:20 krbtgt/EXAMPLE.COM@EXAMPLE.COM
|
||||
|
||||
Now point the configuration of the key in 'akka.conf' to the correct location and set the correct service principal name. The web application should now startup and produce at least a 401 response with a header ``WWW-Authenticate`` = "Negotiate". The last step is to configure the browser.
|
||||
|
||||
6. Set up Firefox to use Kerberos/SPNEGO
|
||||
This is done by typing ``about:config``. Filter the config entries for ``network.neg`` and set the config entries ``network.negotiate-auth.delegation-uris`` and ``network.negotiate-auth.trusted-uris`` to ``localhost``.
|
||||
and now …
|
||||
|
||||
7. Access the RESTful Actor.
|
||||
|
||||
8. Have fun
|
||||
… but acquire an initial ticket for the user principal first: ``kinit zaphod``
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2011 Scalable Solutions AB <http://scalablesolutions.se>
|
||||
*/
|
||||
package akka.http
|
||||
|
||||
import com.sun.jersey.spi.container.servlet.ServletContainer
|
||||
|
||||
/**
|
||||
* This is just a simple wrapper on top of ServletContainer to inject some config from the akka.conf
|
||||
* If you were using akka.comet.AkkaServlet before, but only use it for Jersey, you should switch to this servlet instead
|
||||
*/
|
||||
class AkkaRestServlet extends ServletContainer {
|
||||
import akka.config.Config.{ config ⇒ c }
|
||||
|
||||
val initParams = new java.util.HashMap[String, String]
|
||||
|
||||
addInitParameter("com.sun.jersey.config.property.packages", c.getList("akka.http.resource-packages").mkString(";"))
|
||||
addInitParameter("com.sun.jersey.spi.container.ResourceFilters", c.getList("akka.http.filters").mkString(","))
|
||||
|
||||
/**
|
||||
* Provide a fallback for default values
|
||||
*/
|
||||
override def getInitParameter(key: String) =
|
||||
Option(super.getInitParameter(key)).getOrElse(initParams get key)
|
||||
|
||||
/**
|
||||
* Provide a fallback for default values
|
||||
*/
|
||||
override def getInitParameterNames() = {
|
||||
import scala.collection.JavaConversions._
|
||||
initParams.keySet.iterator ++ super.getInitParameterNames
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide possibility to add config params
|
||||
*/
|
||||
def addInitParameter(param: String, value: String): Unit = initParams.put(param, value)
|
||||
}
|
||||
|
|
@ -1,563 +0,0 @@
|
|||
/*
|
||||
* Copyright 2007-2008 WorldWide Conferencing, LLC
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* AKKA AAS (Authentication and Authorization Service)
|
||||
* Rework of lift's (www.liftweb.com) HTTP Authentication module
|
||||
* All cred to the Lift team (www.liftweb.com), especially David Pollak and Tim Perrett
|
||||
*/
|
||||
|
||||
package akka.security
|
||||
|
||||
import akka.actor.{ Scheduler, Actor, ActorRef, IllegalActorStateException }
|
||||
import akka.event.EventHandler
|
||||
import akka.actor.Actor._
|
||||
import akka.config.{ Config, ConfigurationException }
|
||||
|
||||
import com.sun.jersey.api.model.AbstractMethod
|
||||
import com.sun.jersey.spi.container.{ ResourceFilterFactory, ContainerRequest, ContainerRequestFilter, ContainerResponse, ContainerResponseFilter, ResourceFilter }
|
||||
import com.sun.jersey.core.util.Base64
|
||||
|
||||
import javax.ws.rs.core.{ SecurityContext, Context, Response }
|
||||
import javax.ws.rs.WebApplicationException
|
||||
import javax.annotation.security.{ DenyAll, PermitAll, RolesAllowed }
|
||||
import java.security.Principal
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
case object OK
|
||||
|
||||
/**
|
||||
* Authenticate represents a message to authenticate a request
|
||||
*/
|
||||
case class Authenticate(val req: ContainerRequest, val rolesAllowed: List[String])
|
||||
|
||||
/**
|
||||
* User info represents a sign-on with associated credentials/roles
|
||||
*/
|
||||
case class UserInfo(val username: String, val password: String, val roles: List[String])
|
||||
|
||||
trait Credentials
|
||||
|
||||
case class BasicCredentials(username: String, password: String) extends Credentials
|
||||
|
||||
case class DigestCredentials(method: String,
|
||||
userName: String,
|
||||
realm: String,
|
||||
nonce: String,
|
||||
uri: String,
|
||||
qop: String,
|
||||
nc: String,
|
||||
cnonce: String,
|
||||
response: String,
|
||||
opaque: String) extends Credentials
|
||||
|
||||
case class SpnegoCredentials(token: Array[Byte]) extends Credentials
|
||||
|
||||
/**
|
||||
* Jersey Filter for invocation intercept and authorization/authentication
|
||||
*/
|
||||
class AkkaSecurityFilterFactory extends ResourceFilterFactory {
|
||||
class Filter(actor: ActorRef, rolesAllowed: Option[List[String]])
|
||||
extends ResourceFilter with ContainerRequestFilter {
|
||||
|
||||
override def getRequestFilter: ContainerRequestFilter = this
|
||||
|
||||
override def getResponseFilter: ContainerResponseFilter = null
|
||||
|
||||
/**
|
||||
* Here's where the magic happens. The request is authenticated by
|
||||
* sending a request for authentication to the configured authenticator actor
|
||||
*/
|
||||
override def filter(request: ContainerRequest): ContainerRequest =
|
||||
rolesAllowed match {
|
||||
case Some(roles) ⇒ {
|
||||
val result = (authenticator !! Authenticate(request, roles)).as[AnyRef]
|
||||
result match {
|
||||
case Some(OK) ⇒ request
|
||||
case Some(r) if r.isInstanceOf[Response] ⇒
|
||||
throw new WebApplicationException(r.asInstanceOf[Response])
|
||||
case None ⇒ throw new WebApplicationException(408)
|
||||
case unknown ⇒ {
|
||||
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
case None ⇒ throw new WebApplicationException(Response.Status.FORBIDDEN)
|
||||
}
|
||||
}
|
||||
|
||||
lazy val authenticatorFQN = {
|
||||
val auth = Config.config.getString("akka.http.authenticator", "N/A")
|
||||
if (auth == "N/A") throw new IllegalActorStateException("The config option 'akka.http.authenticator' is not defined in 'akka.conf'")
|
||||
auth
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently we always take the first, since there usually should be at most one authentication actor, but a round-robin
|
||||
* strategy could be implemented in the future
|
||||
*/
|
||||
def authenticator: ActorRef = Actor.registry.actorFor(authenticatorFQN)
|
||||
.getOrElse(throw new ConfigurationException(
|
||||
"akka.http.authenticator configuration option does not have a valid actor address [" + authenticatorFQN + "]"))
|
||||
|
||||
def mkFilter(roles: Option[List[String]]): java.util.List[ResourceFilter] =
|
||||
java.util.Collections.singletonList(new Filter(authenticator, roles))
|
||||
|
||||
/**
|
||||
* The create method is invoked for each resource, and we look for javax.annotation.security annotations
|
||||
* and create the appropriate Filter configurations for each.
|
||||
*/
|
||||
override def create(am: AbstractMethod): java.util.List[ResourceFilter] = {
|
||||
|
||||
//DenyAll takes precedence
|
||||
if (am.isAnnotationPresent(classOf[DenyAll]))
|
||||
return mkFilter(None)
|
||||
|
||||
//Method-level RolesAllowed takes precedence
|
||||
val ra = am.getAnnotation(classOf[RolesAllowed])
|
||||
|
||||
if (ra ne null)
|
||||
return mkFilter(Some(ra.value.toList))
|
||||
|
||||
//PermitAll takes precedence over resource-level RolesAllowed annotation
|
||||
if (am.isAnnotationPresent(classOf[PermitAll]))
|
||||
return null;
|
||||
|
||||
//Last but not least, the resource-level RolesAllowed
|
||||
val cra = am.getResource.getAnnotation(classOf[RolesAllowed])
|
||||
if (cra ne null)
|
||||
return mkFilter(Some(cra.value.toList))
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AuthenticationActor is the super-trait for actors doing Http authentication
|
||||
* It defines the common ground and the flow of execution
|
||||
*/
|
||||
trait AuthenticationActor[C <: Credentials] extends Actor {
|
||||
type Req = ContainerRequest
|
||||
|
||||
//What realm does the authentication use?
|
||||
def realm: String
|
||||
|
||||
//Creates a response to signal unauthorized
|
||||
def unauthorized: Response
|
||||
|
||||
//Used to extract information from the request, returns None if no credentials found
|
||||
def extractCredentials(r: Req): Option[C]
|
||||
|
||||
//returns None is unverified
|
||||
def verify(c: Option[C]): Option[UserInfo]
|
||||
|
||||
//Contruct a new SecurityContext from the supplied parameters
|
||||
def mkSecurityContext(r: Req, user: UserInfo): SecurityContext
|
||||
|
||||
//This is the default security context factory
|
||||
def mkDefaultSecurityContext(r: Req, u: UserInfo, scheme: String): SecurityContext = {
|
||||
val n = u.username
|
||||
val p = new Principal { def getName = n }
|
||||
|
||||
new SecurityContext {
|
||||
def getAuthenticationScheme = scheme
|
||||
def getUserPrincipal = p
|
||||
def isSecure = r.isSecure
|
||||
def isUserInRole(role: String) = u.roles.exists(_ == role)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for the execution flow of authentication
|
||||
*
|
||||
* Credentials are extracted and verified from the request,
|
||||
* and a security context is created for the ContainerRequest
|
||||
* this should ensure good integration with current Jersey security
|
||||
*/
|
||||
protected val authenticate: Receive = {
|
||||
case Authenticate(req, roles) ⇒ {
|
||||
verify(extractCredentials(req)) match {
|
||||
case Some(u: UserInfo) ⇒ {
|
||||
req.setSecurityContext(mkSecurityContext(req, u))
|
||||
if (roles.exists(req.isUserInRole(_))) self.reply(OK)
|
||||
else self.reply(Response.status(Response.Status.FORBIDDEN).build)
|
||||
}
|
||||
case _ ⇒ self.reply(unauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def receive = authenticate
|
||||
|
||||
//returns the string value of the "Authorization"-header of the request
|
||||
def auth(r: Req) = r.getHeaderValue("Authorization")
|
||||
|
||||
//Turns the aforementioned header value into an option
|
||||
def authOption(r: Req): Option[String] = {
|
||||
val a = auth(r)
|
||||
if ((a ne null) && a.length > 0) Some(a) else None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This trait implements the logic for Http Basic authentication
|
||||
* mix this trait into a class to create an authenticator
|
||||
* Don't forget to set the authenticator FQN in the rest-part of the akka config
|
||||
*/
|
||||
trait BasicAuthenticationActor extends AuthenticationActor[BasicCredentials] {
|
||||
override def unauthorized =
|
||||
Response.status(401).header("WWW-Authenticate", "Basic realm=\"" + realm + "\"").build
|
||||
|
||||
override def extractCredentials(r: Req): Option[BasicCredentials] = {
|
||||
val Authorization = """(.*):(.*)""".r
|
||||
|
||||
authOption(r) match {
|
||||
case Some(token) ⇒ {
|
||||
val authResponse = new String(Base64.decode(token.substring(6).getBytes))
|
||||
authResponse match {
|
||||
case Authorization(username, password) ⇒ Some(BasicCredentials(username, password))
|
||||
case _ ⇒ None
|
||||
}
|
||||
}
|
||||
case _ ⇒ None
|
||||
}
|
||||
}
|
||||
|
||||
override def mkSecurityContext(r: Req, u: UserInfo): SecurityContext =
|
||||
mkDefaultSecurityContext(r, u, SecurityContext.BASIC_AUTH)
|
||||
}
|
||||
|
||||
/**
|
||||
* This trait implements the logic for Http Digest authentication mix this trait into a
|
||||
* class to create an authenticator. Don't forget to set the authenticator FQN in the
|
||||
* rest-part of the akka config
|
||||
*/
|
||||
trait DigestAuthenticationActor extends AuthenticationActor[DigestCredentials] {
|
||||
import LiftUtils._
|
||||
|
||||
private object InvalidateNonces
|
||||
|
||||
//Holds the generated nonces for the specified validity period
|
||||
val nonceMap = mkNonceMap
|
||||
|
||||
//Discards old nonces
|
||||
protected val invalidateNonces: Receive = {
|
||||
case InvalidateNonces ⇒
|
||||
val ts = System.currentTimeMillis
|
||||
nonceMap.filter(tuple ⇒ (ts - tuple._2) < nonceValidityPeriod)
|
||||
case unknown ⇒ {}
|
||||
}
|
||||
|
||||
//Schedule the invalidation of nonces
|
||||
Scheduler.schedule(self, InvalidateNonces, noncePurgeInterval, noncePurgeInterval, TimeUnit.MILLISECONDS)
|
||||
|
||||
//authenticate or invalidate nonces
|
||||
override def receive = authenticate orElse invalidateNonces
|
||||
|
||||
override def unauthorized: Response = {
|
||||
val nonce = randomString(64)
|
||||
nonceMap.put(nonce, System.currentTimeMillis)
|
||||
unauthorized(nonce, "auth", randomString(64))
|
||||
}
|
||||
|
||||
def unauthorized(nonce: String, qop: String, opaque: String): Response = {
|
||||
Response.status(401).header(
|
||||
"WWW-Authenticate",
|
||||
"Digest realm=\"" + realm + "\", " +
|
||||
"qop=\"" + qop + "\", " +
|
||||
"nonce=\"" + nonce + "\", " +
|
||||
"opaque=\"" + opaque + "\"").build
|
||||
}
|
||||
|
||||
//Tests wether the specified credentials are valid
|
||||
def validate(auth: DigestCredentials, user: UserInfo): Boolean = {
|
||||
def h(s: String) = hexEncode(md5(s.getBytes("UTF-8")))
|
||||
|
||||
val ha1 = h(auth.userName + ":" + auth.realm + ":" + user.password)
|
||||
val ha2 = h(auth.method + ":" + auth.uri)
|
||||
|
||||
val response = h(
|
||||
ha1 + ":" + auth.nonce + ":" +
|
||||
auth.nc + ":" + auth.cnonce + ":" +
|
||||
auth.qop + ":" + ha2)
|
||||
|
||||
(response == auth.response) && (nonceMap.getOrElse(auth.nonce, -1) != -1)
|
||||
}
|
||||
|
||||
override def verify(odc: Option[DigestCredentials]): Option[UserInfo] = odc match {
|
||||
case Some(dc) ⇒ {
|
||||
userInfo(dc.userName) match {
|
||||
case Some(u) if validate(dc, u) ⇒
|
||||
nonceMap.get(dc.nonce).map(t ⇒ (System.currentTimeMillis - t) < nonceValidityPeriod).map(_ ⇒ u)
|
||||
case _ ⇒ None
|
||||
}
|
||||
}
|
||||
case _ ⇒ None
|
||||
}
|
||||
|
||||
override def extractCredentials(r: Req): Option[DigestCredentials] = {
|
||||
authOption(r).map(s ⇒ {
|
||||
val ? = splitNameValuePairs(s.substring(7, s.length))
|
||||
DigestCredentials(r.getMethod.toUpperCase,
|
||||
?("username"), ?("realm"), ?("nonce"),
|
||||
?("uri"), ?("qop"), ?("nc"),
|
||||
?("cnonce"), ?("response"), ?("opaque"))
|
||||
})
|
||||
}
|
||||
|
||||
override def mkSecurityContext(r: Req, u: UserInfo): SecurityContext =
|
||||
mkDefaultSecurityContext(r, u, SecurityContext.DIGEST_AUTH)
|
||||
|
||||
//Mandatory overrides
|
||||
def userInfo(username: String): Option[UserInfo]
|
||||
|
||||
def mkNonceMap: scala.collection.mutable.Map[String, Long]
|
||||
|
||||
//Optional overrides
|
||||
def nonceValidityPeriod = 60 * 1000 //ms
|
||||
def noncePurgeInterval = 2 * 60 * 1000 //ms
|
||||
}
|
||||
|
||||
import java.security.Principal
|
||||
import java.security.PrivilegedActionException
|
||||
import java.security.PrivilegedExceptionAction
|
||||
|
||||
import javax.security.auth.login.AppConfigurationEntry
|
||||
import javax.security.auth.login.Configuration
|
||||
import javax.security.auth.login.LoginContext
|
||||
import javax.security.auth.Subject
|
||||
import javax.security.auth.kerberos.KerberosPrincipal
|
||||
|
||||
import org.ietf.jgss.GSSContext
|
||||
import org.ietf.jgss.GSSCredential
|
||||
import org.ietf.jgss.GSSManager
|
||||
|
||||
trait SpnegoAuthenticationActor extends AuthenticationActor[SpnegoCredentials] {
|
||||
override def unauthorized =
|
||||
Response.status(401).header("WWW-Authenticate", "Negotiate").build
|
||||
|
||||
// for some reason the jersey Base64 class does not work with kerberos
|
||||
// but the commons Base64 does
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
override def extractCredentials(r: Req): Option[SpnegoCredentials] = {
|
||||
val AuthHeader = """Negotiate\s(.*)""".r
|
||||
|
||||
authOption(r) match {
|
||||
case Some(AuthHeader(token)) ⇒
|
||||
Some(SpnegoCredentials(Base64.decodeBase64(token.trim.getBytes)))
|
||||
case _ ⇒ None
|
||||
}
|
||||
}
|
||||
|
||||
override def verify(odc: Option[SpnegoCredentials]): Option[UserInfo] = odc match {
|
||||
case Some(dc) ⇒ {
|
||||
try {
|
||||
val principal = Subject.doAs(this.serviceSubject, new KerberosValidateAction(dc.token));
|
||||
val user = stripRealmFrom(principal)
|
||||
Some(UserInfo(user, null, rolesFor(user)))
|
||||
} catch {
|
||||
case e: PrivilegedActionException ⇒ {
|
||||
EventHandler.error(e, this, e.getMessage)
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
case _ ⇒ None
|
||||
}
|
||||
|
||||
override def mkSecurityContext(r: Req, u: UserInfo): SecurityContext =
|
||||
mkDefaultSecurityContext(r, u, SecurityContext.CLIENT_CERT_AUTH) // the security context does not know about spnego/kerberos
|
||||
// not sure whether to use a constant from the security context or something like "SPNEGO/Kerberos"
|
||||
|
||||
/**
|
||||
* returns the roles for the given user
|
||||
*/
|
||||
def rolesFor(user: String): List[String]
|
||||
|
||||
// Kerberos
|
||||
|
||||
/**
|
||||
* strips the realm from a kerberos principal name, returning only the user part
|
||||
*/
|
||||
private def stripRealmFrom(principal: String): String = principal.split("@")(0)
|
||||
|
||||
/**
|
||||
* principal name for the HTTP kerberos service, i.e HTTP/ { server } @ { realm }
|
||||
*/
|
||||
lazy val servicePrincipal = {
|
||||
val p = Config.config.getString("akka.http.kerberos.servicePrincipal", "N/A")
|
||||
if (p == "N/A") throw new IllegalActorStateException("The config option 'akka.http.kerberos.servicePrincipal' is not defined in 'akka.conf'")
|
||||
p
|
||||
}
|
||||
|
||||
/**
|
||||
* keytab location with credentials for the service principal
|
||||
*/
|
||||
lazy val keyTabLocation = {
|
||||
val p = Config.config.getString("akka.http.kerberos.keyTabLocation", "N/A")
|
||||
if (p == "N/A") throw new IllegalActorStateException("The config option 'akka.http.kerberos.keyTabLocation' is not defined in 'akka.conf'")
|
||||
p
|
||||
}
|
||||
|
||||
lazy val kerberosDebug = {
|
||||
val p = Config.config.getString("akka.http.kerberos.kerberosDebug", "N/A")
|
||||
if (p == "N/A") throw new IllegalActorStateException("The config option 'akka.http.kerberos.kerberosDebug' is not defined in 'akka.conf'")
|
||||
p
|
||||
}
|
||||
|
||||
/**
|
||||
* is not used by this authenticator, so accept an empty value
|
||||
*/
|
||||
lazy val realm = Config.config.getString("akka.http.kerberos.realm", "")
|
||||
|
||||
/**
|
||||
* verify the kerberos token from a client with the server
|
||||
*/
|
||||
class KerberosValidateAction(kerberosTicket: Array[Byte]) extends PrivilegedExceptionAction[String] {
|
||||
def run = {
|
||||
val context = GSSManager.getInstance().createContext(null.asInstanceOf[GSSCredential])
|
||||
context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length)
|
||||
val user = context.getSrcName().toString()
|
||||
context.dispose()
|
||||
user
|
||||
}
|
||||
}
|
||||
|
||||
// service principal login to kerberos on startup
|
||||
|
||||
val serviceSubject = servicePrincipalLogin
|
||||
|
||||
/**
|
||||
* acquire an initial ticket from the kerberos server for the HTTP service
|
||||
*/
|
||||
def servicePrincipalLogin = {
|
||||
val loginConfig = new LoginConfig(
|
||||
new java.net.URL(this.keyTabLocation).toExternalForm(),
|
||||
this.servicePrincipal,
|
||||
this.kerberosDebug)
|
||||
val princ = new java.util.HashSet[Principal](1)
|
||||
princ.add(new KerberosPrincipal(this.servicePrincipal))
|
||||
val sub = new Subject(false, princ, new java.util.HashSet[Object], new java.util.HashSet[Object])
|
||||
val lc = new LoginContext("", sub, null, loginConfig)
|
||||
lc.login()
|
||||
lc.getSubject()
|
||||
}
|
||||
|
||||
/**
|
||||
* this class simulates a login-config.xml
|
||||
*/
|
||||
class LoginConfig(keyTabLocation: String, servicePrincipal: String, debug: String) extends Configuration {
|
||||
override def getAppConfigurationEntry(name: String): Array[AppConfigurationEntry] = {
|
||||
val options = new java.util.HashMap[String, String]
|
||||
options.put("useKeyTab", "true")
|
||||
options.put("keyTab", this.keyTabLocation)
|
||||
options.put("principal", this.servicePrincipal)
|
||||
options.put("storeKey", "true")
|
||||
options.put("doNotPrompt", "true")
|
||||
options.put("isInitiator", "true")
|
||||
options.put("debug", debug)
|
||||
|
||||
Array(new AppConfigurationEntry(
|
||||
"com.sun.security.auth.module.Krb5LoginModule",
|
||||
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
|
||||
options))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Copyright 2006-2010 WorldWide Conferencing, LLC
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
object LiftUtils {
|
||||
import java.security.{ MessageDigest, SecureRandom }
|
||||
val random = new SecureRandom()
|
||||
|
||||
def md5(in: Array[Byte]): Array[Byte] = (MessageDigest.getInstance("MD5")).digest(in)
|
||||
|
||||
/**
|
||||
* Create a random string of a given size
|
||||
* @param size size of the string to create. Must be a positive or nul integer
|
||||
* @return the generated string
|
||||
*/
|
||||
def randomString(size: Int): String = {
|
||||
def addChar(pos: Int, lastRand: Int, sb: StringBuilder): StringBuilder = {
|
||||
if (pos >= size) sb
|
||||
else {
|
||||
val randNum = if ((pos % 6) == 0) random.nextInt else lastRand
|
||||
sb.append((randNum & 0x1f) match {
|
||||
case n if n < 26 ⇒ ('A' + n).toChar
|
||||
case n ⇒ ('0' + (n - 26)).toChar
|
||||
})
|
||||
addChar(pos + 1, randNum >> 5, sb)
|
||||
}
|
||||
}
|
||||
addChar(0, 0, new StringBuilder(size)).toString
|
||||
}
|
||||
|
||||
/** encode a Byte array as hexadecimal characters */
|
||||
def hexEncode(in: Array[Byte]): String = {
|
||||
val sb = new StringBuilder
|
||||
val len = in.length
|
||||
def addDigit(in: Array[Byte], pos: Int, len: Int, sb: StringBuilder) {
|
||||
if (pos < len) {
|
||||
val b: Int = in(pos)
|
||||
val msb = (b & 0xf0) >> 4
|
||||
val lsb = (b & 0x0f)
|
||||
sb.append((if (msb < 10) ('0' + msb).asInstanceOf[Char] else ('a' + (msb - 10)).asInstanceOf[Char]))
|
||||
sb.append((if (lsb < 10) ('0' + lsb).asInstanceOf[Char] else ('a' + (lsb - 10)).asInstanceOf[Char]))
|
||||
addDigit(in, pos + 1, len, sb)
|
||||
}
|
||||
}
|
||||
addDigit(in, 0, len, sb)
|
||||
sb.toString
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a string of the form <name1=value1, name2=value2, ... > and unquotes the quoted values.
|
||||
* The result is a Map[String, String]
|
||||
*/
|
||||
def splitNameValuePairs(props: String): Map[String, String] = {
|
||||
/**
|
||||
* If str is surrounded by quotes it return the content between the quotes
|
||||
*/
|
||||
def unquote(str: String) = {
|
||||
if ((str ne null) && str.length >= 2 && str.charAt(0) == '\"' && str.charAt(str.length - 1) == '\"')
|
||||
str.substring(1, str.length - 1)
|
||||
else
|
||||
str
|
||||
}
|
||||
|
||||
val list = props.split(",").toList.map(in ⇒ {
|
||||
val pair = in match { case null ⇒ Nil case s ⇒ s.split("=").toList.map(_.trim).filter(_.length > 0) }
|
||||
(pair(0), unquote(pair(1)))
|
||||
})
|
||||
val map: Map[String, String] = Map.empty
|
||||
(map /: list)((m, next) ⇒ m + (next))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package akka.security
|
||||
|
||||
import junit.framework.Test
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestSuite
|
||||
|
||||
object AllTest extends TestCase {
|
||||
def suite(): Test = {
|
||||
val suite = new TestSuite("All Scala tests")
|
||||
suite.addTestSuite(classOf[BasicAuthenticatorSpec])
|
||||
suite
|
||||
}
|
||||
|
||||
def main(args: Array[String]) = junit.textui.TestRunner.run(suite)
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2011 Scalable Solutions AB <http://scalablesolutions.se>
|
||||
*/
|
||||
|
||||
package akka.security
|
||||
|
||||
import akka.config.Supervision._
|
||||
import akka.actor.Actor._
|
||||
|
||||
import org.scalatest.Suite
|
||||
import org.scalatest.junit.JUnitSuite
|
||||
import org.scalatest.matchers.MustMatchers
|
||||
import org.scalatest.mock.MockitoSugar
|
||||
import org.mockito.Mockito._
|
||||
import org.mockito.Matchers._
|
||||
import org.junit.{ Before, After, Test }
|
||||
|
||||
import javax.ws.rs.core.{ SecurityContext, Context, Response }
|
||||
import com.sun.jersey.spi.container.{ ResourceFilterFactory, ContainerRequest, ContainerRequestFilter, ContainerResponse, ContainerResponseFilter, ResourceFilter }
|
||||
import com.sun.jersey.core.util.Base64
|
||||
|
||||
object BasicAuthenticatorSpec {
|
||||
class BasicAuthenticator extends BasicAuthenticationActor {
|
||||
def verify(odc: Option[BasicCredentials]): Option[UserInfo] = odc match {
|
||||
case Some(dc) ⇒ Some(UserInfo("foo", "bar", "ninja" :: "chef" :: Nil))
|
||||
case _ ⇒ None
|
||||
}
|
||||
override def realm = "test"
|
||||
}
|
||||
}
|
||||
|
||||
class BasicAuthenticatorSpec extends junit.framework.TestCase
|
||||
with Suite with MockitoSugar with MustMatchers {
|
||||
import BasicAuthenticatorSpec._
|
||||
|
||||
val authenticator = actorOf[BasicAuthenticator]
|
||||
authenticator.start()
|
||||
|
||||
@Test
|
||||
def testChallenge = {
|
||||
val req = mock[ContainerRequest]
|
||||
|
||||
val result = (authenticator !! (Authenticate(req, List("foo")), 10000)).as[Response].get
|
||||
|
||||
// the actor replies with a challenge for the browser
|
||||
result.getStatus must equal(Response.Status.UNAUTHORIZED.getStatusCode)
|
||||
result.getMetadata.get("WWW-Authenticate").get(0).toString must startWith("Basic")
|
||||
}
|
||||
|
||||
@Test
|
||||
def testAuthenticationSuccess = {
|
||||
val req = mock[ContainerRequest]
|
||||
// fake a basic auth header -> this will authenticate the user
|
||||
when(req.getHeaderValue("Authorization")).thenReturn("Basic " + new String(Base64.encode("foo:bar")))
|
||||
|
||||
// fake a request authorization -> this will authorize the user
|
||||
when(req.isUserInRole("chef")).thenReturn(true)
|
||||
|
||||
val result = (authenticator !! (Authenticate(req, List("chef")), 10000)).as[AnyRef].get
|
||||
|
||||
result must be(OK)
|
||||
// the authenticator must have set a security context
|
||||
verify(req).setSecurityContext(any[SecurityContext])
|
||||
}
|
||||
|
||||
@Test
|
||||
def testUnauthorized = {
|
||||
val req = mock[ContainerRequest]
|
||||
|
||||
// fake a basic auth header -> this will authenticate the user
|
||||
when(req.getHeaderValue("Authorization")).thenReturn("Basic " + new String(Base64.encode("foo:bar")))
|
||||
when(req.isUserInRole("chef")).thenReturn(false) // this will deny access
|
||||
|
||||
val result = (authenticator !! (Authenticate(req, List("chef")), 10000)).as[Response].get
|
||||
|
||||
result.getStatus must equal(Response.Status.FORBIDDEN.getStatusCode)
|
||||
|
||||
// the authenticator must have set a security context
|
||||
verify(req).setSecurityContext(any[SecurityContext])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -204,31 +204,6 @@ akka {
|
|||
hostname = "localhost"
|
||||
port = 9998
|
||||
|
||||
#If you are using akka.http.AkkaRestServlet
|
||||
filters = ["akka.security.AkkaSecurityFilterFactory"] # List with all jersey filters to use
|
||||
# resource-packages = ["sample.rest.scala",
|
||||
# "sample.rest.java",
|
||||
# "sample.security"] # List with all resource packages for your Jersey services
|
||||
resource-packages = []
|
||||
|
||||
# The authentication service to use. Need to be overridden (sample now)
|
||||
# authenticator = "sample.security.BasicAuthenticationService"
|
||||
authenticator = "N/A"
|
||||
|
||||
# Uncomment if you are using the KerberosAuthenticationActor
|
||||
# kerberos {
|
||||
# servicePrincipal = "HTTP/localhost@EXAMPLE.COM"
|
||||
# keyTabLocation = "URL to keytab"
|
||||
# kerberosDebug = "true"
|
||||
# realm = "EXAMPLE.COM"
|
||||
# }
|
||||
kerberos {
|
||||
servicePrincipal = "N/A"
|
||||
keyTabLocation = "N/A"
|
||||
kerberosDebug = "N/A"
|
||||
realm = ""
|
||||
}
|
||||
|
||||
# If you are using akka.http.AkkaMistServlet
|
||||
mist-dispatcher {
|
||||
#type = "GlobalDispatcher" # Uncomment if you want to use a different dispatcher than the default one for Comet
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue