0
# OpenID Authentication
1
2
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.
3
4
## Capabilities
5
6
### OpenID Client - Scala API
7
8
Main entry point for OpenID authentication operations.
9
10
```scala { .api }
11
/**
12
* OpenID authentication client
13
*/
14
object OpenID {
15
/**
16
* Generate OpenID authentication redirect URL
17
* @param openID The OpenID identifier (user's OpenID URL or provider URL)
18
* @param callbackURL URL where the OpenID provider should redirect after authentication
19
* @param axRequired Required attribute exchange parameters
20
* @param axOptional Optional attribute exchange parameters
21
* @param realm Optional realm for the authentication request
22
* @return Future containing the redirect URL
23
*/
24
def redirectURL(
25
openID: String,
26
callbackURL: String,
27
axRequired: Seq[(String, String)] = Seq.empty,
28
axOptional: Seq[(String, String)] = Seq.empty,
29
realm: Option[String] = None
30
)(implicit app: Application): Future[String]
31
32
/**
33
* Verify OpenID authentication response and extract user information
34
* @param request HTTP request containing OpenID response parameters
35
* @return Future containing verified user information
36
*/
37
def verifiedId(implicit request: Request[_], app: Application): Future[UserInfo]
38
}
39
```
40
41
**Basic OpenID Flow Example:**
42
43
```scala
44
import play.api.libs.openid._
45
import play.api.mvc._
46
import scala.concurrent.Future
47
import scala.concurrent.ExecutionContext.Implicits.global
48
49
class OpenIDController @Inject()(cc: ControllerComponents)
50
extends AbstractController(cc) {
51
52
def login = Action.async { implicit request =>
53
val openIdUrl = "https://www.google.com/accounts/o8/id"
54
val callbackURL = routes.OpenIDController.callback().absoluteURL()
55
56
// Request email and name attributes
57
val axRequired = Seq(
58
"email" -> "http://axschema.org/contact/email",
59
"firstname" -> "http://axschema.org/namePerson/first",
60
"lastname" -> "http://axschema.org/namePerson/last"
61
)
62
63
OpenID.redirectURL(
64
openID = openIdUrl,
65
callbackURL = callbackURL,
66
axRequired = axRequired
67
).map { redirectUrl =>
68
Redirect(redirectUrl)
69
}.recover {
70
case ex =>
71
BadRequest(s"OpenID error: ${ex.getMessage}")
72
}
73
}
74
75
def callback = Action.async { implicit request =>
76
OpenID.verifiedId.map { userInfo =>
77
// User successfully authenticated
78
Ok(s"Welcome ${userInfo.id}!")
79
.withSession("openid" -> userInfo.id)
80
}.recover {
81
case ex =>
82
BadRequest(s"Authentication failed: ${ex.getMessage}")
83
}
84
}
85
}
86
```
87
88
### OpenID Client - Java API
89
90
Main entry point for OpenID authentication operations in Java.
91
92
```java { .api }
93
/**
94
* OpenID authentication client for Java
95
*/
96
public class OpenID {
97
/**
98
* Generate OpenID authentication redirect URL
99
* @param openID The OpenID identifier (user's OpenID URL or provider URL)
100
* @param callbackURL URL where the OpenID provider should redirect after authentication
101
* @param axRequired Required attribute exchange parameters
102
* @param axOptional Optional attribute exchange parameters
103
* @param realm Optional realm for the authentication request
104
* @return Promise containing the redirect URL
105
*/
106
public static Promise<String> redirectURL(
107
String openID,
108
String callbackURL,
109
Map<String, String> axRequired,
110
Map<String, String> axOptional,
111
String realm
112
)
113
114
public static Promise<String> redirectURL(String openID, String callbackURL)
115
116
/**
117
* Verify OpenID authentication response and extract user information
118
* @param request HTTP request containing OpenID response parameters
119
* @return Promise containing verified user information
120
*/
121
public static Promise<UserInfo> verifiedId(Http.Request request)
122
}
123
```
124
125
**Java OpenID Flow Example:**
126
127
```java
128
import play.libs.openid.*;
129
import play.libs.F.Promise;
130
import play.mvc.*;
131
132
public class AuthController extends Controller {
133
134
public Result login() {
135
return ok(views.html.login.render());
136
}
137
138
public Promise<Result> authenticate() {
139
// Step 1: Generate redirect URL for OpenID provider
140
String openidURL = "https://www.google.com/accounts/o8/id";
141
String callbackURL = routes.AuthController.openidCallback().absoluteURL(request());
142
143
// Set up attribute exchange
144
Map<String, String> axRequired = new HashMap<>();
145
axRequired.put("email", "http://axschema.org/contact/email");
146
axRequired.put("firstname", "http://axschema.org/namePerson/first");
147
axRequired.put("lastname", "http://axschema.org/namePerson/last");
148
149
return OpenID.redirectURL(openidURL, callbackURL, axRequired, new HashMap<>(), null)
150
.map(url -> redirect(url));
151
}
152
153
public Promise<Result> openidCallback() {
154
// Step 2: Verify the OpenID response
155
return OpenID.verifiedId(request())
156
.map(userInfo -> {
157
// Authentication successful - create session
158
return redirect(routes.Application.index())
159
.withSession("openid", userInfo.getId())
160
.withSession("email", userInfo.getAttributes().get("email"));
161
})
162
.recover(throwable -> {
163
return badRequest("Authentication failed: " + throwable.getMessage());
164
});
165
}
166
}
167
```
168
169
### OpenID Client Interface - Scala API
170
171
Injectable OpenID client interface for dependency injection.
172
173
```scala { .api }
174
/**
175
* OpenID client interface for dependency injection
176
*/
177
trait OpenIdClient {
178
/**
179
* Generate authentication redirect URL
180
*/
181
def redirectURL(
182
openID: String,
183
callbackURL: String,
184
axRequired: Seq[(String, String)] = Seq.empty,
185
axOptional: Seq[(String, String)] = Seq.empty,
186
realm: Option[String] = None
187
): Future[String]
188
189
/**
190
* Verify authentication response
191
*/
192
def verifiedId(request: RequestHeader): Future[UserInfo]
193
}
194
```
195
196
### User Information
197
198
User information returned after successful OpenID authentication.
199
200
```scala { .api }
201
/**
202
* OpenID user information
203
* @param id The verified OpenID identifier
204
* @param attributes Additional user attributes from attribute exchange
205
*/
206
case class UserInfo(id: String, attributes: Map[String, String] = Map.empty)
207
```
208
209
**User Information Usage:**
210
211
```scala
212
OpenID.verifiedId.map { userInfo =>
213
val openIdIdentifier = userInfo.id
214
val email = userInfo.attributes.get("email")
215
val firstName = userInfo.attributes.get("firstname")
216
val lastName = userInfo.attributes.get("lastname")
217
218
// Create or update user profile
219
val user = User(
220
openId = openIdIdentifier,
221
email = email.getOrElse(""),
222
name = s"${firstName.getOrElse("")} ${lastName.getOrElse("")}".trim
223
)
224
225
// Store user session
226
Ok("Login successful").withSession("user_id" -> user.id.toString)
227
}
228
```
229
230
### OpenID Server Discovery
231
232
OpenID server information discovered from provider endpoints.
233
234
```scala { .api }
235
/**
236
* OpenID server information
237
* @param protocolVersion OpenID protocol version
238
* @param url Server endpoint URL
239
* @param delegate Optional delegate identifier
240
*/
241
case class OpenIDServer(
242
protocolVersion: String,
243
url: String,
244
delegate: Option[String]
245
)
246
```
247
248
### Discovery Interface
249
250
Server discovery interface for OpenID provider endpoints.
251
252
```scala { .api }
253
/**
254
* OpenID server discovery interface
255
*/
256
trait Discovery {
257
/**
258
* Discover OpenID server information from provider
259
* @param openID OpenID identifier or provider URL
260
* @return Future containing server information
261
*/
262
def discoverServer(openID: String): Future[OpenIDServer]
263
264
/**
265
* Normalize OpenID identifier according to specification
266
* @param openID Raw OpenID identifier
267
* @return Normalized identifier
268
*/
269
def normalizeIdentifier(openID: String): String
270
}
271
```
272
273
### Error Handling
274
275
OpenID-specific error types and handling.
276
277
```scala { .api }
278
/**
279
* OpenID error base class
280
*/
281
sealed abstract class OpenIDError extends Throwable
282
283
/**
284
* OpenID error types
285
*/
286
object Errors {
287
case object MISSING_PARAMETERS extends OpenIDError
288
case object AUTH_ERROR extends OpenIDError
289
case object AUTH_CANCEL extends OpenIDError
290
case object BAD_RESPONSE extends OpenIDError
291
case object NO_SERVER extends OpenIDError
292
case object NETWORK_ERROR extends OpenIDError
293
}
294
```
295
296
**Error Handling Example:**
297
298
```scala
299
import play.api.libs.openid.Errors._
300
301
OpenID.verifiedId.map { userInfo =>
302
// Success case
303
Ok(s"Welcome ${userInfo.id}")
304
}.recover {
305
case AUTH_CANCEL =>
306
Redirect(routes.Application.index())
307
.flashing("warning" -> "Authentication was cancelled")
308
309
case AUTH_ERROR =>
310
BadRequest("Authentication failed")
311
312
case NO_SERVER =>
313
BadRequest("Invalid OpenID provider")
314
315
case NETWORK_ERROR =>
316
InternalServerError("Network error during authentication")
317
318
case ex: OpenIDError =>
319
BadRequest(s"OpenID error: ${ex.getClass.getSimpleName}")
320
321
case ex =>
322
InternalServerError(s"Unexpected error: ${ex.getMessage}")
323
}
324
```
325
326
### Attribute Exchange
327
328
Request user attributes from OpenID providers supporting attribute exchange.
329
330
**Common Attribute Exchange Parameters:**
331
332
```scala
333
// Standard AX schema URLs
334
val axAttributes = Map(
335
"email" -> "http://axschema.org/contact/email",
336
"firstname" -> "http://axschema.org/namePerson/first",
337
"lastname" -> "http://axschema.org/namePerson/last",
338
"fullname" -> "http://axschema.org/namePerson",
339
"nickname" -> "http://axschema.org/namePerson/friendly",
340
"country" -> "http://axschema.org/contact/country/home",
341
"language" -> "http://axschema.org/pref/language",
342
"timezone" -> "http://axschema.org/pref/timezone"
343
)
344
345
// Request required attributes
346
val axRequired = Seq(
347
"email" -> axAttributes("email"),
348
"name" -> axAttributes("fullname")
349
)
350
351
// Request optional attributes
352
val axOptional = Seq(
353
"nickname" -> axAttributes("nickname"),
354
"country" -> axAttributes("country")
355
)
356
357
OpenID.redirectURL(
358
openID = "https://www.google.com/accounts/o8/id",
359
callbackURL = callbackURL,
360
axRequired = axRequired,
361
axOptional = axOptional
362
)
363
```
364
365
### Provider-Specific Examples
366
367
**Google OpenID:**
368
369
```scala
370
def loginWithGoogle = Action.async { implicit request =>
371
val googleOpenId = "https://www.google.com/accounts/o8/id"
372
val callbackURL = routes.OpenIDController.googleCallback().absoluteURL()
373
374
val axRequired = Seq(
375
"email" -> "http://axschema.org/contact/email",
376
"firstname" -> "http://axschema.org/namePerson/first",
377
"lastname" -> "http://axschema.org/namePerson/last"
378
)
379
380
OpenID.redirectURL(googleOpenId, callbackURL, axRequired).map { url =>
381
Redirect(url)
382
}
383
}
384
```
385
386
**Yahoo OpenID:**
387
388
```scala
389
def loginWithYahoo = Action.async { implicit request =>
390
val yahooOpenId = "https://me.yahoo.com"
391
val callbackURL = routes.OpenIDController.yahooCallback().absoluteURL()
392
393
val axRequired = Seq(
394
"email" -> "http://axschema.org/contact/email",
395
"fullname" -> "http://axschema.org/namePerson"
396
)
397
398
OpenID.redirectURL(yahooOpenId, callbackURL, axRequired).map { url =>
399
Redirect(url)
400
}
401
}
402
```
403
404
**Custom OpenID Provider:**
405
406
```scala
407
def loginWithCustom(openIdUrl: String) = Action.async { implicit request =>
408
val callbackURL = routes.OpenIDController.customCallback().absoluteURL()
409
410
// Validate OpenID URL format
411
if (!openIdUrl.startsWith("http://") && !openIdUrl.startsWith("https://")) {
412
Future.successful(BadRequest("Invalid OpenID URL"))
413
} else {
414
OpenID.redirectURL(openIdUrl, callbackURL).map { url =>
415
Redirect(url)
416
}.recover {
417
case ex =>
418
BadRequest(s"Invalid OpenID provider: ${ex.getMessage}")
419
}
420
}
421
}
422
```
423
424
### Complete OpenID Integration
425
426
```scala
427
import play.api.libs.openid._
428
import play.api.mvc._
429
import play.api.data._
430
import play.api.data.Forms._
431
import scala.concurrent.Future
432
import scala.concurrent.ExecutionContext.Implicits.global
433
434
class OpenIDAuthController @Inject()(cc: ControllerComponents)
435
extends AbstractController(cc) {
436
437
val openIdForm = Form(
438
"openid_identifier" -> nonEmptyText
439
)
440
441
def loginForm = Action { implicit request =>
442
Ok(views.html.openid.login(openIdForm))
443
}
444
445
def authenticate = Action.async { implicit request =>
446
openIdForm.bindFromRequest.fold(
447
formWithErrors => {
448
Future.successful(BadRequest(views.html.openid.login(formWithErrors)))
449
},
450
openIdIdentifier => {
451
val callbackURL = routes.OpenIDAuthController.callback().absoluteURL()
452
453
// Standard attributes to request
454
val axRequired = Seq(
455
"email" -> "http://axschema.org/contact/email"
456
)
457
458
val axOptional = Seq(
459
"firstname" -> "http://axschema.org/namePerson/first",
460
"lastname" -> "http://axschema.org/namePerson/last",
461
"fullname" -> "http://axschema.org/namePerson"
462
)
463
464
OpenID.redirectURL(
465
openID = openIdIdentifier,
466
callbackURL = callbackURL,
467
axRequired = axRequired,
468
axOptional = axOptional,
469
realm = Some(request.host)
470
).map { redirectUrl =>
471
Redirect(redirectUrl)
472
}.recover {
473
case ex =>
474
BadRequest(views.html.openid.login(
475
openIdForm.withGlobalError(s"OpenID error: ${ex.getMessage}")
476
))
477
}
478
}
479
)
480
}
481
482
def callback = Action.async { implicit request =>
483
OpenID.verifiedId.map { userInfo =>
484
// Extract user information
485
val openId = userInfo.id
486
val email = userInfo.attributes.get("email").getOrElse("")
487
val name = userInfo.attributes.get("fullname")
488
.orElse(userInfo.attributes.get("firstname").map(_ + " " + userInfo.attributes.get("lastname").getOrElse("")))
489
.getOrElse("Unknown")
490
491
// In a real app, you would save this to your user database
492
// For this example, we'll just store in session
493
Redirect(routes.Application.profile())
494
.withSession(
495
"openid" -> openId,
496
"email" -> email,
497
"name" -> name
498
)
499
.flashing("success" -> "Successfully logged in with OpenID")
500
501
}.recover {
502
case Errors.AUTH_CANCEL =>
503
Redirect(routes.OpenIDAuthController.loginForm())
504
.flashing("warning" -> "Authentication was cancelled")
505
506
case Errors.AUTH_ERROR =>
507
Redirect(routes.OpenIDAuthController.loginForm())
508
.flashing("error" -> "Authentication failed")
509
510
case ex =>
511
Redirect(routes.OpenIDAuthController.loginForm())
512
.flashing("error" -> s"Authentication error: ${ex.getMessage}")
513
}
514
}
515
516
def logout = Action { implicit request =>
517
Redirect(routes.Application.index())
518
.withNewSession
519
.flashing("success" -> "You have been logged out")
520
}
521
}