All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.twitter.finagle.http.SpnegoAuthenticator.scala Maven / Gradle / Ivy

package com.twitter.finagle.http

import com.twitter.finagle.{Service, Filter}
import com.twitter.util.{Base64StringEncoder, Future, FuturePool, Return}
import com.twitter.logging.Logger
import java.security.PrivilegedAction
import javax.security.auth.Subject
import javax.security.auth.login.LoginContext
import org.ietf.jgss._

/**
 * A SPNEGO HTTP authenticator as defined in https://tools.ietf.org/html/rfc4559, which gets
 * its credentials from a provided CredentialSource... usually JAAS.
 */
object SpnegoAuthenticator {
  private val log = Logger("spnego")

  type Token = Array[Byte]
  object Token {
    val Empty = Array.empty[Byte]
  }

  val AuthScheme = "Negotiate"

  private object AuthHeader {
    val SchemePrefixLength = AuthScheme.length + 1
    def apply(token: Option[Token]): Option[String] =
      token map { t =>
        AuthScheme + " " + Base64StringEncoder.encode(t)
      }

    /** If the header represents a valid spnego negotiation, return it. */
    def unapply(header: String): Option[Token] =
      // must be a valid Negotiate header, and have a token
      if (header.length <= SchemePrefixLength || !header.startsWith(AuthScheme)) {
        None
      } else {
        val tokenStr = header.substring(SchemePrefixLength)
        Some(Base64StringEncoder.decode(tokenStr))
      }
  }

  /** An authenticated HTTP request, with its GSS context. */
  trait Authenticated[Req] {
    val request: Req
    val context: GSSContext
  }

  object Authenticated {
    case class Http(request: Request, context: GSSContext) extends Authenticated[Request]
  }

  case class Negotiated(
    established: Option[GSSContext],
    wwwAuthenticate: Option[String])

  object Credentials {
    trait ServerSource {
      /** Loads a GSSContext (for previously specified identifiers).  */
      def load(): Future[GSSContext]
      /**
       * Called by a server to decide whether to accept the given token to create a context.
       * Returns a Negotitated representing the response.
       */
      def accept(context: GSSContext, negotiation: Token): Future[Negotiated]
    }

    trait ClientSource {
      /** Loads a GSSContext (for previously specified identifiers).  */
      def load(): Future[GSSContext]
      /**
       * Called by a client to initialize the security context and return the next Token
       * to send to the server. ChallengeToken may be empty if we haven't been challenged.
       */
      def init(context: GSSContext, challengeToken: Option[Token]): Future[Token]
    }

    /**
     * JAAS implementation of credential fetching/validation (via the configuration section named by
     * loginContext).  If a principal or OID is provided, it is used to override the settings in the JAAS
     * configuration file.
     *
     * Since the authentication operations may block when i.e. talking to a KDC, these potentially
     * blocking calls are wrapped in a FuturePool.
     */
    object JAAS {
      /**
       * Oid for the KRB5 mechanism  These come from
       * http://www.oid-info.com/get/1.2.840.113554.1.2.2
       *
       */
      val Krb5Mechanism = new Oid("1.2.840.113554.1.2.2")
      val Krb5PrincipalType = new Oid("1.2.840.113554.1.2.2.1")
      /**
       * Oid for the Spnego mechanism  These come from
       * http://www.oid-info.com/get/1.3.6.1.5.5.2
       *
       */
      val SpnegoMechanism = new Oid("1.3.6.1.5.5.2")
    }

    trait JAAS {
      val loginContext: String

      def load(): Future[GSSContext] = pool {
        log.debug("Getting context: %s", loginContext)
        val portal = new LoginContext(loginContext)
        // TODO: should logout?
        portal.login()
        Subject.doAs(portal.getSubject, createContextAction)
      }

      private val createContextAction =
        new PrivilegedAction[GSSContext] {
          def run(): GSSContext = createGSSContext()
        }

      /** Called while running with the privileges of the given loginContext.  */
      protected def createGSSContext(): GSSContext

      /** A processes' own principal is usually specified via {{sun.security.krb5.principal}}. */
      protected def selfPrincipal: Option[GSSName] = None

      protected def lifetime: Int = GSSContext.DEFAULT_LIFETIME
      protected def mechanism: Oid = JAAS.Krb5Mechanism
      protected def manager: GSSManager = GSSManager.getInstance
      protected def pool: FuturePool = FuturePool.unboundedPool
    }

    class JAASClientSource(
      val loginContext: String,
      _serverPrincipal: String,
      _serverPrincipalType: Oid = JAAS.Krb5PrincipalType
    ) extends ClientSource with JAAS {
      val serverPrincipal = manager.createName(_serverPrincipal, _serverPrincipalType)

      def init(context: GSSContext, challengeToken: Option[Token]): Future[Token] = pool {
        val tokenIn = challengeToken.getOrElse(Token.Empty)
        var tokenOut: Token = null
        do {
          tokenOut = context.initSecContext(tokenIn, 0, tokenIn.length)
        } while (tokenOut == null);
        tokenOut
      }

      protected def createGSSContext(): GSSContext =
        manager.createContext(
          serverPrincipal,
          mechanism,
          manager.createCredential(
            selfPrincipal.orNull,
            lifetime,
            mechanism,
            GSSCredential.INITIATE_ONLY
          ),
          lifetime
        )
    }

    class JAASServerSource(val loginContext: String) extends ServerSource with JAAS {
      def accept(context: GSSContext, negotiation: Token): Future[Negotiated] = pool {
        val token = context.acceptSecContext(negotiation, 0, negotiation.length)
        val established = if (context.isEstablished) Some(context) else None
        val wwwAuthenticate = AuthHeader(Option(token))
        Negotiated(established, wwwAuthenticate)
      }

      protected def createGSSContext(): GSSContext = {
        val cred = manager.createCredential(
          selfPrincipal.orNull,
          lifetime,
          JAAS.SpnegoMechanism,
          GSSCredential.ACCEPT_ONLY
        )
        cred.add(
          selfPrincipal.orNull,
          lifetime,
          lifetime,
          JAAS.Krb5Mechanism,
          GSSCredential.ACCEPT_ONLY
        )
        manager.createContext(cred)
      }
    }
  }

  /**
   * A typeclass to get/set fields of http responses of type Rsp.
   * TODO: Remove after http and http are merged
   */
  sealed trait RspSupport[Rsp] {
    /** Get the status for the Rsp. */
    def status(rsp: Rsp): Status
    /** Get the WWW-Authenticate: Negotiate  header. */
    def wwwAuthenticateHeader(rsp: Rsp): Option[String]
    /** Sets the WWW-Authenticate: Negotiate  header. */
    def wwwAuthenticateHeader(rsp: Rsp, auth: String): Unit
    /** Create an Unauthorized response with the given protocolVersion. */
    def unauthorized(version: Version): Rsp
  }

  /**
   * A typeclass to get/set fields of http requests of type Req.
   * TODO: Remove after http and http are merged
   */
  sealed trait ReqSupport[Req] {
    /** Returns the AUTHORIZATION header for the given Req. */
    def authorizationHeader(req: Req): Option[String]
    /** Sets the AUTHORIZATION header for the given Req. */
    def authorizationHeader(req: Req, token: Token): Unit
    /** Get the protocol version for a request. */
    def protocolVersion(req: Req): Version
    /** Wrap a Req with authentication information. */
    def authenticated(req: Req, context: GSSContext): Authenticated[Req]
  }

  implicit val httpResponseSupport = new RspSupport[Response] {
    def status(rsp: Response) = rsp.status
    def wwwAuthenticateHeader(rsp: Response) =
      Option(rsp.headers.get(Fields.WwwAuthenticate))
    def wwwAuthenticateHeader(rsp: Response, auth: String) =
      rsp.headers.set(Fields.WwwAuthenticate, auth)
    def unauthorized(version: Version) =  Response(version, Status.Unauthorized)
  }

  implicit val httpRequestSupport = new ReqSupport[Request] {
    def authorizationHeader(req: Request) =
      Option(req.headers.get(Fields.Authorization))
    def authorizationHeader(req: Request, token: Token) =
      AuthHeader(Some(token)).foreach { header =>
        req.headers.set(Fields.Authorization, header)
      }
    def protocolVersion(req: Request) = req.version
    def authenticated(req: Request, context: GSSContext) =
      Authenticated.Http(req, context)
  }

  sealed abstract class Client[Req: ReqSupport, Rsp: RspSupport]
      extends Filter[Req, Rsp, Req, Rsp] {
    val credSrc: Credentials.ClientSource
    val reqs = implicitly[ReqSupport[Req]]
    val rsps = implicitly[RspSupport[Rsp]]

    /**
     * TODO: naive implementation: every request arriving at the filter begins from the
     * beginning of the protocol.
     */
    def apply(req: Req, backend: Service[Req, Rsp]): Future[Rsp] =
      challengeResponseLoop(req, backend, None)

    /** Repeats the challenge/response loop until GSS Tokens are accepted by the server. */
    private def challengeResponseLoop(
      req: Req,
      backend: Service[Req,Rsp],
      credentialOption: Option[Future[GSSContext]]
    ): Future[Rsp] =
      backend(req).transform {
        case Return(rsp) if rsps.status(rsp) == Status.Unauthorized =>
          // we've been challenged: reattempt the request with an improved token
          val credentialFuture = credentialOption.getOrElse(credSrc.load())
          credentialFuture.flatMap { context =>
            // look for any Token data in the challenge, and attempt to initialize
            val challengeToken =
              rsps.wwwAuthenticateHeader(rsp).collect {
                case AuthHeader(token) => token
              }
            credSrc.init(context, challengeToken).flatMap { nextToken =>
              // loop to reattempt the request, mutated with the next token data
              reqs.authorizationHeader(req, nextToken)
              challengeResponseLoop(req, backend, Some(credentialFuture))
            }
          }
        case rsp =>
          // no challenge (or an exception): we're finished
          Future.const(rsp)
      }
  }

  sealed abstract class Server[Req: ReqSupport, Rsp: RspSupport]
      extends Filter[Req, Rsp, SpnegoAuthenticator.Authenticated[Req], Rsp] {
    val credSrc: Credentials.ServerSource
    val reqs = implicitly[ReqSupport[Req]]
    val rsps = implicitly[RspSupport[Rsp]]

    private def unauthorized(req: Req) = {
      val rsp = rsps.unauthorized(reqs.protocolVersion(req))
      rsps.wwwAuthenticateHeader(rsp, AuthScheme)
      rsp
    }

    final def apply(req: Req, authed: Service[Authenticated[Req], Rsp]): Future[Rsp] =
      reqs.authorizationHeader(req).collect {
        case AuthHeader(negotiation) =>
          credSrc.load() flatMap {
            credSrc.accept(_, negotiation)
          } flatMap { negotiated =>
            negotiated.established map { ctx =>
              authed(reqs.authenticated(req, ctx))
            } getOrElse {
              Future value unauthorized(req)
            } map { rsp =>
              negotiated.wwwAuthenticate foreach {
                rsps.wwwAuthenticateHeader(rsp, _)
              }
              rsp
            }
          } handle {
            case e: GSSException => {
              log.error(e, "authenticating")
              unauthorized(req)
            }
          }
      } getOrElse {
        log.debug("Request had no AuthHeader information.  Returning Unauthorized.")
        Future value unauthorized(req)
      }
  }

  case class ClientFilter(credSrc: Credentials.ClientSource)
      extends Client[Request, Response]

  case class ServerFilter(credSrc: Credentials.ServerSource)
      extends Server[Request,  Response]

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy