Asynchronous HTTP client for Play Framework with OAuth, OpenID, and SSL/TLS support
—
OpenID authentication support for identity verification and user attribute exchange. Play WS provides comprehensive OpenID support for authenticating users with OpenID providers like Google, Yahoo, and others, with both Scala and Java APIs.
Main entry point for OpenID authentication operations.
/**
* OpenID authentication client
*/
object OpenID {
/**
* Generate OpenID authentication redirect URL
* @param openID The OpenID identifier (user's OpenID URL or provider URL)
* @param callbackURL URL where the OpenID provider should redirect after authentication
* @param axRequired Required attribute exchange parameters
* @param axOptional Optional attribute exchange parameters
* @param realm Optional realm for the authentication request
* @return Future containing the redirect URL
*/
def redirectURL(
openID: String,
callbackURL: String,
axRequired: Seq[(String, String)] = Seq.empty,
axOptional: Seq[(String, String)] = Seq.empty,
realm: Option[String] = None
)(implicit app: Application): Future[String]
/**
* Verify OpenID authentication response and extract user information
* @param request HTTP request containing OpenID response parameters
* @return Future containing verified user information
*/
def verifiedId(implicit request: Request[_], app: Application): Future[UserInfo]
}Basic OpenID Flow Example:
import play.api.libs.openid._
import play.api.mvc._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
class OpenIDController @Inject()(cc: ControllerComponents)
extends AbstractController(cc) {
def login = Action.async { implicit request =>
val openIdUrl = "https://www.google.com/accounts/o8/id"
val callbackURL = routes.OpenIDController.callback().absoluteURL()
// Request email and name attributes
val axRequired = Seq(
"email" -> "http://axschema.org/contact/email",
"firstname" -> "http://axschema.org/namePerson/first",
"lastname" -> "http://axschema.org/namePerson/last"
)
OpenID.redirectURL(
openID = openIdUrl,
callbackURL = callbackURL,
axRequired = axRequired
).map { redirectUrl =>
Redirect(redirectUrl)
}.recover {
case ex =>
BadRequest(s"OpenID error: ${ex.getMessage}")
}
}
def callback = Action.async { implicit request =>
OpenID.verifiedId.map { userInfo =>
// User successfully authenticated
Ok(s"Welcome ${userInfo.id}!")
.withSession("openid" -> userInfo.id)
}.recover {
case ex =>
BadRequest(s"Authentication failed: ${ex.getMessage}")
}
}
}Main entry point for OpenID authentication operations in Java.
/**
* OpenID authentication client for Java
*/
public class OpenID {
/**
* Generate OpenID authentication redirect URL
* @param openID The OpenID identifier (user's OpenID URL or provider URL)
* @param callbackURL URL where the OpenID provider should redirect after authentication
* @param axRequired Required attribute exchange parameters
* @param axOptional Optional attribute exchange parameters
* @param realm Optional realm for the authentication request
* @return Promise containing the redirect URL
*/
public static Promise<String> redirectURL(
String openID,
String callbackURL,
Map<String, String> axRequired,
Map<String, String> axOptional,
String realm
)
public static Promise<String> redirectURL(String openID, String callbackURL)
/**
* Verify OpenID authentication response and extract user information
* @param request HTTP request containing OpenID response parameters
* @return Promise containing verified user information
*/
public static Promise<UserInfo> verifiedId(Http.Request request)
}Java OpenID Flow Example:
import play.libs.openid.*;
import play.libs.F.Promise;
import play.mvc.*;
public class AuthController extends Controller {
public Result login() {
return ok(views.html.login.render());
}
public Promise<Result> authenticate() {
// Step 1: Generate redirect URL for OpenID provider
String openidURL = "https://www.google.com/accounts/o8/id";
String callbackURL = routes.AuthController.openidCallback().absoluteURL(request());
// Set up attribute exchange
Map<String, String> axRequired = new HashMap<>();
axRequired.put("email", "http://axschema.org/contact/email");
axRequired.put("firstname", "http://axschema.org/namePerson/first");
axRequired.put("lastname", "http://axschema.org/namePerson/last");
return OpenID.redirectURL(openidURL, callbackURL, axRequired, new HashMap<>(), null)
.map(url -> redirect(url));
}
public Promise<Result> openidCallback() {
// Step 2: Verify the OpenID response
return OpenID.verifiedId(request())
.map(userInfo -> {
// Authentication successful - create session
return redirect(routes.Application.index())
.withSession("openid", userInfo.getId())
.withSession("email", userInfo.getAttributes().get("email"));
})
.recover(throwable -> {
return badRequest("Authentication failed: " + throwable.getMessage());
});
}
}Injectable OpenID client interface for dependency injection.
/**
* OpenID client interface for dependency injection
*/
trait OpenIdClient {
/**
* Generate authentication redirect URL
*/
def redirectURL(
openID: String,
callbackURL: String,
axRequired: Seq[(String, String)] = Seq.empty,
axOptional: Seq[(String, String)] = Seq.empty,
realm: Option[String] = None
): Future[String]
/**
* Verify authentication response
*/
def verifiedId(request: RequestHeader): Future[UserInfo]
}User information returned after successful OpenID authentication.
/**
* OpenID user information
* @param id The verified OpenID identifier
* @param attributes Additional user attributes from attribute exchange
*/
case class UserInfo(id: String, attributes: Map[String, String] = Map.empty)User Information Usage:
OpenID.verifiedId.map { userInfo =>
val openIdIdentifier = userInfo.id
val email = userInfo.attributes.get("email")
val firstName = userInfo.attributes.get("firstname")
val lastName = userInfo.attributes.get("lastname")
// Create or update user profile
val user = User(
openId = openIdIdentifier,
email = email.getOrElse(""),
name = s"${firstName.getOrElse("")} ${lastName.getOrElse("")}".trim
)
// Store user session
Ok("Login successful").withSession("user_id" -> user.id.toString)
}OpenID server information discovered from provider endpoints.
/**
* OpenID server information
* @param protocolVersion OpenID protocol version
* @param url Server endpoint URL
* @param delegate Optional delegate identifier
*/
case class OpenIDServer(
protocolVersion: String,
url: String,
delegate: Option[String]
)Server discovery interface for OpenID provider endpoints.
/**
* OpenID server discovery interface
*/
trait Discovery {
/**
* Discover OpenID server information from provider
* @param openID OpenID identifier or provider URL
* @return Future containing server information
*/
def discoverServer(openID: String): Future[OpenIDServer]
/**
* Normalize OpenID identifier according to specification
* @param openID Raw OpenID identifier
* @return Normalized identifier
*/
def normalizeIdentifier(openID: String): String
}OpenID-specific error types and handling.
/**
* OpenID error base class
*/
sealed abstract class OpenIDError extends Throwable
/**
* OpenID error types
*/
object Errors {
case object MISSING_PARAMETERS extends OpenIDError
case object AUTH_ERROR extends OpenIDError
case object AUTH_CANCEL extends OpenIDError
case object BAD_RESPONSE extends OpenIDError
case object NO_SERVER extends OpenIDError
case object NETWORK_ERROR extends OpenIDError
}Error Handling Example:
import play.api.libs.openid.Errors._
OpenID.verifiedId.map { userInfo =>
// Success case
Ok(s"Welcome ${userInfo.id}")
}.recover {
case AUTH_CANCEL =>
Redirect(routes.Application.index())
.flashing("warning" -> "Authentication was cancelled")
case AUTH_ERROR =>
BadRequest("Authentication failed")
case NO_SERVER =>
BadRequest("Invalid OpenID provider")
case NETWORK_ERROR =>
InternalServerError("Network error during authentication")
case ex: OpenIDError =>
BadRequest(s"OpenID error: ${ex.getClass.getSimpleName}")
case ex =>
InternalServerError(s"Unexpected error: ${ex.getMessage}")
}Request user attributes from OpenID providers supporting attribute exchange.
Common Attribute Exchange Parameters:
// Standard AX schema URLs
val axAttributes = Map(
"email" -> "http://axschema.org/contact/email",
"firstname" -> "http://axschema.org/namePerson/first",
"lastname" -> "http://axschema.org/namePerson/last",
"fullname" -> "http://axschema.org/namePerson",
"nickname" -> "http://axschema.org/namePerson/friendly",
"country" -> "http://axschema.org/contact/country/home",
"language" -> "http://axschema.org/pref/language",
"timezone" -> "http://axschema.org/pref/timezone"
)
// Request required attributes
val axRequired = Seq(
"email" -> axAttributes("email"),
"name" -> axAttributes("fullname")
)
// Request optional attributes
val axOptional = Seq(
"nickname" -> axAttributes("nickname"),
"country" -> axAttributes("country")
)
OpenID.redirectURL(
openID = "https://www.google.com/accounts/o8/id",
callbackURL = callbackURL,
axRequired = axRequired,
axOptional = axOptional
)Google OpenID:
def loginWithGoogle = Action.async { implicit request =>
val googleOpenId = "https://www.google.com/accounts/o8/id"
val callbackURL = routes.OpenIDController.googleCallback().absoluteURL()
val axRequired = Seq(
"email" -> "http://axschema.org/contact/email",
"firstname" -> "http://axschema.org/namePerson/first",
"lastname" -> "http://axschema.org/namePerson/last"
)
OpenID.redirectURL(googleOpenId, callbackURL, axRequired).map { url =>
Redirect(url)
}
}Yahoo OpenID:
def loginWithYahoo = Action.async { implicit request =>
val yahooOpenId = "https://me.yahoo.com"
val callbackURL = routes.OpenIDController.yahooCallback().absoluteURL()
val axRequired = Seq(
"email" -> "http://axschema.org/contact/email",
"fullname" -> "http://axschema.org/namePerson"
)
OpenID.redirectURL(yahooOpenId, callbackURL, axRequired).map { url =>
Redirect(url)
}
}Custom OpenID Provider:
def loginWithCustom(openIdUrl: String) = Action.async { implicit request =>
val callbackURL = routes.OpenIDController.customCallback().absoluteURL()
// Validate OpenID URL format
if (!openIdUrl.startsWith("http://") && !openIdUrl.startsWith("https://")) {
Future.successful(BadRequest("Invalid OpenID URL"))
} else {
OpenID.redirectURL(openIdUrl, callbackURL).map { url =>
Redirect(url)
}.recover {
case ex =>
BadRequest(s"Invalid OpenID provider: ${ex.getMessage}")
}
}
}import play.api.libs.openid._
import play.api.mvc._
import play.api.data._
import play.api.data.Forms._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
class OpenIDAuthController @Inject()(cc: ControllerComponents)
extends AbstractController(cc) {
val openIdForm = Form(
"openid_identifier" -> nonEmptyText
)
def loginForm = Action { implicit request =>
Ok(views.html.openid.login(openIdForm))
}
def authenticate = Action.async { implicit request =>
openIdForm.bindFromRequest.fold(
formWithErrors => {
Future.successful(BadRequest(views.html.openid.login(formWithErrors)))
},
openIdIdentifier => {
val callbackURL = routes.OpenIDAuthController.callback().absoluteURL()
// Standard attributes to request
val axRequired = Seq(
"email" -> "http://axschema.org/contact/email"
)
val axOptional = Seq(
"firstname" -> "http://axschema.org/namePerson/first",
"lastname" -> "http://axschema.org/namePerson/last",
"fullname" -> "http://axschema.org/namePerson"
)
OpenID.redirectURL(
openID = openIdIdentifier,
callbackURL = callbackURL,
axRequired = axRequired,
axOptional = axOptional,
realm = Some(request.host)
).map { redirectUrl =>
Redirect(redirectUrl)
}.recover {
case ex =>
BadRequest(views.html.openid.login(
openIdForm.withGlobalError(s"OpenID error: ${ex.getMessage}")
))
}
}
)
}
def callback = Action.async { implicit request =>
OpenID.verifiedId.map { userInfo =>
// Extract user information
val openId = userInfo.id
val email = userInfo.attributes.get("email").getOrElse("")
val name = userInfo.attributes.get("fullname")
.orElse(userInfo.attributes.get("firstname").map(_ + " " + userInfo.attributes.get("lastname").getOrElse("")))
.getOrElse("Unknown")
// In a real app, you would save this to your user database
// For this example, we'll just store in session
Redirect(routes.Application.profile())
.withSession(
"openid" -> openId,
"email" -> email,
"name" -> name
)
.flashing("success" -> "Successfully logged in with OpenID")
}.recover {
case Errors.AUTH_CANCEL =>
Redirect(routes.OpenIDAuthController.loginForm())
.flashing("warning" -> "Authentication was cancelled")
case Errors.AUTH_ERROR =>
Redirect(routes.OpenIDAuthController.loginForm())
.flashing("error" -> "Authentication failed")
case ex =>
Redirect(routes.OpenIDAuthController.loginForm())
.flashing("error" -> s"Authentication error: ${ex.getMessage}")
}
}
def logout = Action { implicit request =>
Redirect(routes.Application.index())
.withNewSession
.flashing("success" -> "You have been logged out")
}
}Install with Tessl CLI
npx tessl i tessl/maven-com-typesafe-play--play-ws-2-10