Line data Source code
1 : // Copyright (C) 2009 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.httpd.auth.openid;
16 :
17 : import com.google.common.flogger.FluentLogger;
18 : import com.google.gerrit.common.Nullable;
19 : import com.google.gerrit.common.PageLinks;
20 : import com.google.gerrit.common.auth.openid.OpenIdUrls;
21 : import com.google.gerrit.entities.Account;
22 : import com.google.gerrit.entities.KeyUtil;
23 : import com.google.gerrit.extensions.registration.DynamicItem;
24 : import com.google.gerrit.extensions.restapi.Url;
25 : import com.google.gerrit.httpd.CanonicalWebUrl;
26 : import com.google.gerrit.httpd.ProxyProperties;
27 : import com.google.gerrit.httpd.WebSession;
28 : import com.google.gerrit.server.IdentifiedUser;
29 : import com.google.gerrit.server.UrlEncoded;
30 : import com.google.gerrit.server.account.AccountException;
31 : import com.google.gerrit.server.account.AccountManager;
32 : import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
33 : import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
34 : import com.google.gerrit.server.config.AuthConfig;
35 : import com.google.gerrit.server.config.ConfigUtil;
36 : import com.google.gerrit.server.config.GerritServerConfig;
37 : import com.google.inject.Inject;
38 : import com.google.inject.Provider;
39 : import com.google.inject.Singleton;
40 : import java.io.IOException;
41 : import java.net.URL;
42 : import java.util.List;
43 : import java.util.Optional;
44 : import java.util.concurrent.TimeUnit;
45 : import javax.servlet.http.Cookie;
46 : import javax.servlet.http.HttpServletRequest;
47 : import javax.servlet.http.HttpServletResponse;
48 : import org.eclipse.jgit.lib.Config;
49 : import org.openid4java.consumer.ConsumerException;
50 : import org.openid4java.consumer.ConsumerManager;
51 : import org.openid4java.consumer.VerificationResult;
52 : import org.openid4java.discovery.DiscoveryException;
53 : import org.openid4java.discovery.DiscoveryInformation;
54 : import org.openid4java.message.AuthRequest;
55 : import org.openid4java.message.Message;
56 : import org.openid4java.message.MessageException;
57 : import org.openid4java.message.MessageExtension;
58 : import org.openid4java.message.ParameterList;
59 : import org.openid4java.message.ax.AxMessage;
60 : import org.openid4java.message.ax.FetchRequest;
61 : import org.openid4java.message.ax.FetchResponse;
62 : import org.openid4java.message.pape.PapeMessage;
63 : import org.openid4java.message.pape.PapeRequest;
64 : import org.openid4java.message.pape.PapeResponse;
65 : import org.openid4java.message.sreg.SRegMessage;
66 : import org.openid4java.message.sreg.SRegRequest;
67 : import org.openid4java.message.sreg.SRegResponse;
68 : import org.openid4java.util.HttpClientFactory;
69 :
70 : @Singleton
71 : class OpenIdServiceImpl {
72 99 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
73 :
74 : static final String RETURN_URL = "OpenID";
75 :
76 : private static final String P_MODE = "gerrit.mode";
77 : private static final String P_TOKEN = "gerrit.token";
78 : private static final String P_REMEMBER = "gerrit.remember";
79 : private static final String P_CLAIMED = "gerrit.claimed";
80 : private static final int LASTID_AGE = 365 * 24 * 60 * 60; // seconds
81 :
82 : private static final String OPENID_MODE = "openid.mode";
83 : private static final String OMODE_CANCEL = "cancel";
84 :
85 : private static final String SCHEMA_EMAIL = "http://schema.openid.net/contact/email";
86 : private static final String SCHEMA_FIRSTNAME = "http://schema.openid.net/namePerson/first";
87 : private static final String SCHEMA_LASTNAME = "http://schema.openid.net/namePerson/last";
88 :
89 : private final DynamicItem<WebSession> webSession;
90 : private final Provider<IdentifiedUser> identifiedUser;
91 : private final CanonicalWebUrl urlProvider;
92 : private final AccountManager accountManager;
93 : private final ConsumerManager manager;
94 : private final List<OpenIdProviderPattern> allowedOpenIDs;
95 : private final List<String> openIdDomains;
96 : private final ExternalIdKeyFactory externalIdKeyFactory;
97 : private final com.google.gerrit.server.account.AuthRequest.Factory authRequestFactory;
98 :
99 : /** Maximum age, in seconds, before forcing re-authentication of account. */
100 : private final int papeMaxAuthAge;
101 :
102 : @Inject
103 : OpenIdServiceImpl(
104 : DynamicItem<WebSession> cf,
105 : Provider<IdentifiedUser> iu,
106 : CanonicalWebUrl up,
107 : @GerritServerConfig Config config,
108 : AuthConfig ac,
109 : AccountManager am,
110 : ProxyProperties proxyProperties,
111 : ExternalIdKeyFactory externalIdKeyFactory,
112 99 : com.google.gerrit.server.account.AuthRequest.Factory authRequestFactory) {
113 :
114 99 : if (proxyProperties.getProxyUrl() != null) {
115 0 : final org.openid4java.util.ProxyProperties proxy = new org.openid4java.util.ProxyProperties();
116 0 : URL url = proxyProperties.getProxyUrl();
117 0 : proxy.setProxyHostName(url.getHost());
118 0 : proxy.setProxyPort(url.getPort());
119 0 : proxy.setUserName(proxyProperties.getUsername());
120 0 : proxy.setPassword(proxyProperties.getPassword());
121 0 : HttpClientFactory.setProxyProperties(proxy);
122 : }
123 :
124 99 : webSession = cf;
125 99 : identifiedUser = iu;
126 99 : urlProvider = up;
127 99 : accountManager = am;
128 99 : manager = new ConsumerManager();
129 99 : allowedOpenIDs = ac.getAllowedOpenIDs();
130 99 : openIdDomains = ac.getOpenIdDomains();
131 99 : papeMaxAuthAge =
132 : (int)
133 99 : ConfigUtil.getTimeUnit(
134 : config, //
135 : "auth",
136 : null,
137 : "maxOpenIdSessionAge",
138 : -1,
139 : TimeUnit.SECONDS);
140 99 : this.externalIdKeyFactory = externalIdKeyFactory;
141 99 : this.authRequestFactory = authRequestFactory;
142 99 : }
143 :
144 : @SuppressWarnings("unchecked")
145 : DiscoveryResult discover(
146 : HttpServletRequest req,
147 : String openidIdentifier,
148 : SignInMode mode,
149 : boolean remember,
150 : String returnToken) {
151 : final State state;
152 0 : state = init(req, openidIdentifier, mode, remember, returnToken);
153 0 : if (state == null) {
154 0 : return new DiscoveryResult(DiscoveryResult.Status.NO_PROVIDER);
155 : }
156 :
157 : final AuthRequest aReq;
158 : try {
159 0 : aReq = manager.authenticate(state.discovered, state.retTo.toString());
160 0 : logger.atFine().log("OpenID: openid-realm=%s", state.contextUrl);
161 0 : aReq.setRealm(state.contextUrl);
162 :
163 0 : if (requestRegistration(aReq)) {
164 0 : final SRegRequest sregReq = SRegRequest.createFetchRequest();
165 0 : sregReq.addAttribute("fullname", true);
166 0 : sregReq.addAttribute("email", true);
167 0 : aReq.addExtension(sregReq);
168 :
169 0 : final FetchRequest fetch = FetchRequest.createFetchRequest();
170 0 : fetch.addAttribute("FirstName", SCHEMA_FIRSTNAME, true);
171 0 : fetch.addAttribute("LastName", SCHEMA_LASTNAME, true);
172 0 : fetch.addAttribute("Email", SCHEMA_EMAIL, true);
173 0 : aReq.addExtension(fetch);
174 : }
175 :
176 0 : if (0 <= papeMaxAuthAge) {
177 0 : final PapeRequest pape = PapeRequest.createPapeRequest();
178 0 : pape.setMaxAuthAge(papeMaxAuthAge);
179 0 : aReq.addExtension(pape);
180 : }
181 0 : } catch (MessageException | ConsumerException e) {
182 0 : logger.atSevere().withCause(e).log("Cannot create OpenID redirect for %s", openidIdentifier);
183 0 : return new DiscoveryResult(DiscoveryResult.Status.ERROR);
184 0 : }
185 :
186 0 : return new DiscoveryResult(aReq.getDestinationUrl(false), aReq.getParameterMap());
187 : }
188 :
189 : private boolean requestRegistration(AuthRequest aReq) {
190 0 : if (AuthRequest.SELECT_ID.equals(aReq.getIdentity())) {
191 : // We don't know anything about the identity, as the provider
192 : // will offer the user a way to indicate their identity. Skip
193 : // any database query operation and assume we must ask for the
194 : // registration information, in case the identity is new to us.
195 : //
196 0 : return true;
197 : }
198 :
199 : // We might already have this account on file. Look for it.
200 : //
201 : try {
202 0 : return !accountManager.lookup(aReq.getIdentity()).isPresent();
203 0 : } catch (AccountException e) {
204 0 : logger.atWarning().withCause(e).log("Cannot determine if user account exists");
205 0 : return true;
206 : }
207 : }
208 :
209 : /** Called by {@link OpenIdLoginServlet} doGet, doPost */
210 : void doAuth(HttpServletRequest req, HttpServletResponse rsp) throws Exception {
211 0 : if (OMODE_CANCEL.equals(req.getParameter(OPENID_MODE))) {
212 0 : cancel(req, rsp);
213 0 : return;
214 : }
215 :
216 : // Process the authentication response.
217 : //
218 0 : final SignInMode mode = signInMode(req);
219 0 : final String openidIdentifier = req.getParameter("openid.identity");
220 0 : final String claimedIdentifier = req.getParameter(P_CLAIMED);
221 0 : final String returnToken = req.getParameter(P_TOKEN);
222 0 : final boolean remember = "1".equals(req.getParameter(P_REMEMBER));
223 : final String rediscoverIdentifier =
224 0 : claimedIdentifier != null ? claimedIdentifier : openidIdentifier;
225 : final State state;
226 :
227 0 : if (!isAllowedOpenID(rediscoverIdentifier)
228 0 : || !isAllowedOpenID(openidIdentifier)
229 0 : || (claimedIdentifier != null && !isAllowedOpenID(claimedIdentifier))) {
230 0 : cancelWithError(req, rsp, "Provider not allowed");
231 0 : return;
232 : }
233 :
234 0 : state = init(req, rediscoverIdentifier, mode, remember, returnToken);
235 0 : if (state == null) {
236 : // Re-discovery must have failed, we can't run a login.
237 : //
238 0 : cancel(req, rsp);
239 0 : return;
240 : }
241 :
242 0 : final String returnTo = req.getParameter("openid.return_to");
243 0 : if (returnTo != null && returnTo.contains("openid.rpnonce=")) {
244 : // Some providers (claimid.com) seem to embed these request
245 : // parameters into our return_to URL, and then give us them
246 : // in the return_to request parameter. But not all.
247 : //
248 0 : state.retTo.put("openid.rpnonce", req.getParameter("openid.rpnonce"));
249 0 : state.retTo.put("openid.rpsig", req.getParameter("openid.rpsig"));
250 : }
251 :
252 0 : final VerificationResult result =
253 0 : manager.verify(
254 0 : state.retTo.toString(), new ParameterList(req.getParameterMap()), state.discovered);
255 0 : if (result.getVerifiedId() == null /* authentication failure */) {
256 0 : if ("Nonce verification failed.".equals(result.getStatusMsg())) {
257 : // We might be suffering from clock skew on this system.
258 : //
259 0 : logger.atSevere().log(
260 : "OpenID failure: %s Likely caused by clock skew on this server,"
261 : + " install/configure NTP.",
262 0 : result.getStatusMsg());
263 0 : cancelWithError(req, rsp, result.getStatusMsg());
264 :
265 0 : } else if (result.getStatusMsg() != null) {
266 : // Authentication failed.
267 : //
268 0 : logger.atSevere().log("OpenID failure: %s", result.getStatusMsg());
269 0 : cancelWithError(req, rsp, result.getStatusMsg());
270 :
271 : } else {
272 : // Assume authentication was canceled.
273 : //
274 0 : cancel(req, rsp);
275 : }
276 0 : return;
277 : }
278 :
279 0 : final Message authRsp = result.getAuthResponse();
280 0 : SRegResponse sregRsp = null;
281 0 : FetchResponse fetchRsp = null;
282 :
283 0 : if (0 <= papeMaxAuthAge) {
284 : PapeResponse ext;
285 0 : boolean unsupported = false;
286 :
287 : try {
288 0 : ext = (PapeResponse) authRsp.getExtension(PapeMessage.OPENID_NS_PAPE);
289 0 : } catch (MessageException err) {
290 : // Far too many providers are unable to provide PAPE extensions
291 : // right now. Instead of blocking all of them log the error and
292 : // let the authentication complete anyway.
293 : //
294 0 : logger.atSevere().withCause(err).log("Invalid PAPE response from %s", openidIdentifier);
295 0 : unsupported = true;
296 0 : ext = null;
297 0 : }
298 0 : if (!unsupported && ext == null) {
299 0 : logger.atSevere().log("No PAPE extension response from %s", openidIdentifier);
300 0 : cancelWithError(req, rsp, "OpenID provider does not support PAPE.");
301 0 : return;
302 : }
303 : }
304 :
305 0 : if (authRsp.hasExtension(SRegMessage.OPENID_NS_SREG)) {
306 0 : final MessageExtension ext = authRsp.getExtension(SRegMessage.OPENID_NS_SREG);
307 0 : if (ext instanceof SRegResponse) {
308 0 : sregRsp = (SRegResponse) ext;
309 : }
310 : }
311 :
312 0 : if (authRsp.hasExtension(AxMessage.OPENID_NS_AX)) {
313 0 : final MessageExtension ext = authRsp.getExtension(AxMessage.OPENID_NS_AX);
314 0 : if (ext instanceof FetchResponse) {
315 0 : fetchRsp = (FetchResponse) ext;
316 : }
317 : }
318 :
319 0 : final com.google.gerrit.server.account.AuthRequest areq =
320 0 : authRequestFactory.create(externalIdKeyFactory.parse(openidIdentifier));
321 :
322 0 : if (sregRsp != null) {
323 0 : areq.setDisplayName(sregRsp.getAttributeValue("fullname"));
324 0 : areq.setEmailAddress(sregRsp.getAttributeValue("email"));
325 :
326 0 : } else if (fetchRsp != null) {
327 0 : final String firstName = fetchRsp.getAttributeValue("FirstName");
328 0 : final String lastName = fetchRsp.getAttributeValue("LastName");
329 0 : final StringBuilder n = new StringBuilder();
330 0 : if (firstName != null && firstName.length() > 0) {
331 0 : n.append(firstName);
332 : }
333 0 : if (lastName != null && lastName.length() > 0) {
334 0 : if (n.length() > 0) {
335 0 : n.append(' ');
336 : }
337 0 : n.append(lastName);
338 : }
339 0 : areq.setDisplayName(n.length() > 0 ? n.toString() : null);
340 0 : areq.setEmailAddress(fetchRsp.getAttributeValue("Email"));
341 : }
342 :
343 0 : if (openIdDomains != null && !openIdDomains.isEmpty()) {
344 : // Administrator limited email domains, which can be used for OpenID.
345 : // Login process will only work if the passed email matches one
346 : // of these domains.
347 : //
348 0 : final String email = areq.getEmailAddress();
349 0 : int emailAtIndex = email.lastIndexOf('@');
350 0 : if (emailAtIndex >= 0 && emailAtIndex < email.length() - 1) {
351 0 : final String emailDomain = email.substring(emailAtIndex);
352 :
353 0 : boolean match = false;
354 0 : for (String domain : openIdDomains) {
355 0 : if (emailDomain.equalsIgnoreCase(domain)) {
356 0 : match = true;
357 0 : break;
358 : }
359 0 : }
360 :
361 0 : if (!match) {
362 0 : logger.atSevere().log("Domain disallowed: %s", emailDomain);
363 0 : cancelWithError(req, rsp, "Domain disallowed");
364 0 : return;
365 : }
366 : }
367 : }
368 :
369 0 : if (claimedIdentifier != null) {
370 : // The user used a claimed identity which has delegated to the verified
371 : // identity we have in our AuthRequest above. We still should have a
372 : // link between the two, so set one up if not present.
373 : //
374 0 : Optional<Account.Id> claimedId = accountManager.lookup(claimedIdentifier);
375 0 : Optional<Account.Id> actualId = accountManager.lookup(areq.getExternalIdKey().get());
376 :
377 0 : if (claimedId.isPresent() && actualId.isPresent()) {
378 0 : if (claimedId.get().equals(actualId.get())) {
379 : // Both link to the same account, that's what we expected.
380 : } else {
381 : // This is (for now) a fatal error. There are two records
382 : // for what might be the same user.
383 : //
384 0 : logger.atSevere().log(
385 : "OpenID accounts disagree over user identity:\n"
386 : + " Claimed ID: %s is %s\n"
387 : + " Delgate ID: %s is %s",
388 0 : claimedId.get(), claimedIdentifier, actualId.get(), areq.getExternalIdKey());
389 0 : cancelWithError(req, rsp, "Contact site administrator");
390 0 : return;
391 : }
392 :
393 0 : } else if (!claimedId.isPresent() && actualId.isPresent()) {
394 : // Older account, the actual was already created but the claimed
395 : // was missing due to a bug in Gerrit. Link the claimed.
396 : //
397 0 : final com.google.gerrit.server.account.AuthRequest linkReq =
398 0 : authRequestFactory.create(externalIdKeyFactory.parse(claimedIdentifier));
399 0 : linkReq.setDisplayName(areq.getDisplayName());
400 0 : linkReq.setEmailAddress(areq.getEmailAddress());
401 0 : accountManager.link(actualId.get(), linkReq);
402 :
403 0 : } else if (claimedId.isPresent() && !actualId.isPresent()) {
404 : // Claimed account already exists, but it smells like the user has
405 : // changed their delegate to point to a different provider. Link
406 : // the new provider.
407 : //
408 0 : accountManager.link(claimedId.get(), areq);
409 :
410 : } else {
411 : // Both are null, we are going to create a new account below.
412 : }
413 : }
414 :
415 : try {
416 : final com.google.gerrit.server.account.AuthResult arsp;
417 0 : switch (mode) {
418 : case REGISTER:
419 : case SIGN_IN:
420 0 : arsp = accountManager.authenticate(areq);
421 :
422 0 : final Cookie lastId = new Cookie(OpenIdUrls.LASTID_COOKIE, "");
423 0 : lastId.setPath(req.getContextPath() + "/login/");
424 0 : if (remember) {
425 0 : lastId.setValue(rediscoverIdentifier);
426 0 : lastId.setMaxAge(LASTID_AGE);
427 : } else {
428 0 : lastId.setMaxAge(0);
429 : }
430 0 : rsp.addCookie(lastId);
431 0 : webSession.get().login(arsp, remember);
432 0 : if (arsp.isNew() && claimedIdentifier != null) {
433 0 : final com.google.gerrit.server.account.AuthRequest linkReq =
434 0 : authRequestFactory.create(externalIdKeyFactory.parse(claimedIdentifier));
435 0 : linkReq.setDisplayName(areq.getDisplayName());
436 0 : linkReq.setEmailAddress(areq.getEmailAddress());
437 0 : accountManager.link(arsp.getAccountId(), linkReq);
438 : }
439 0 : callback(arsp.isNew(), req, rsp);
440 0 : break;
441 :
442 : case LINK_IDENTIY:
443 : {
444 0 : arsp = accountManager.link(identifiedUser.get().getAccountId(), areq);
445 0 : webSession.get().login(arsp, remember);
446 0 : callback(false, req, rsp);
447 : break;
448 : }
449 : }
450 0 : } catch (AccountException e) {
451 0 : logger.atSevere().withCause(e).log("OpenID authentication failure");
452 0 : cancelWithError(req, rsp, "Contact site administrator");
453 0 : }
454 0 : }
455 :
456 : private boolean isSignIn(SignInMode mode) {
457 0 : switch (mode) {
458 : case SIGN_IN:
459 : case REGISTER:
460 0 : return true;
461 : case LINK_IDENTIY:
462 : default:
463 0 : return false;
464 : }
465 : }
466 :
467 : private static SignInMode signInMode(HttpServletRequest req) {
468 : try {
469 0 : return SignInMode.valueOf(req.getParameter(P_MODE));
470 0 : } catch (RuntimeException e) {
471 0 : return SignInMode.SIGN_IN;
472 : }
473 : }
474 :
475 : private void callback(final boolean isNew, HttpServletRequest req, HttpServletResponse rsp)
476 : throws IOException {
477 0 : String token = req.getParameter(P_TOKEN);
478 0 : if (token == null || token.isEmpty() || token.startsWith("/SignInFailure,")) {
479 0 : token = PageLinks.MINE;
480 : }
481 :
482 0 : final StringBuilder rdr = new StringBuilder();
483 0 : rdr.append(urlProvider.get(req));
484 0 : String nextToken = Url.decode(token);
485 0 : String registerUri = PageLinks.REGISTER + "/";
486 0 : if (isNew && !token.startsWith(registerUri)) {
487 0 : rdr.append('#' + registerUri);
488 0 : if (nextToken.startsWith("#")) {
489 : // Need to strip the leading # off the token to fix registration page redirect
490 0 : nextToken = nextToken.substring(1);
491 : }
492 : }
493 0 : rdr.append(nextToken);
494 0 : rsp.sendRedirect(rdr.toString());
495 0 : }
496 :
497 : private void cancel(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
498 0 : if (isSignIn(signInMode(req))) {
499 0 : webSession.get().logout();
500 : }
501 0 : callback(false, req, rsp);
502 0 : }
503 :
504 : private void cancelWithError(
505 : final HttpServletRequest req, HttpServletResponse rsp, String errorDetail)
506 : throws IOException {
507 0 : final SignInMode mode = signInMode(req);
508 0 : if (isSignIn(mode)) {
509 0 : webSession.get().logout();
510 : }
511 0 : final StringBuilder rdr = new StringBuilder();
512 0 : rdr.append(urlProvider.get(req));
513 0 : rdr.append('#');
514 0 : rdr.append("SignInFailure");
515 0 : rdr.append(',');
516 0 : rdr.append(mode.name());
517 0 : rdr.append(',');
518 0 : rdr.append(errorDetail != null ? KeyUtil.encode(errorDetail) : "");
519 0 : rsp.sendRedirect(rdr.toString());
520 0 : }
521 :
522 : @Nullable
523 : private State init(
524 : HttpServletRequest req,
525 : final String openidIdentifier,
526 : final SignInMode mode,
527 : final boolean remember,
528 : final String returnToken) {
529 : final List<?> list;
530 : try {
531 0 : list = manager.discover(openidIdentifier);
532 0 : } catch (DiscoveryException e) {
533 0 : logger.atSevere().withCause(e).log("Cannot discover OpenID %s", openidIdentifier);
534 0 : return null;
535 0 : }
536 0 : if (list == null || list.isEmpty()) {
537 0 : return null;
538 : }
539 :
540 0 : final String contextUrl = urlProvider.get(req);
541 0 : final DiscoveryInformation discovered = manager.associate(list);
542 0 : final UrlEncoded retTo = new UrlEncoded(contextUrl + RETURN_URL);
543 0 : retTo.put(P_MODE, mode.name());
544 0 : if (returnToken != null && returnToken.length() > 0) {
545 0 : retTo.put(P_TOKEN, returnToken);
546 : }
547 0 : if (remember) {
548 0 : retTo.put(P_REMEMBER, "1");
549 : }
550 0 : if (discovered.hasClaimedIdentifier()) {
551 0 : retTo.put(P_CLAIMED, discovered.getClaimedIdentifier().getIdentifier());
552 : }
553 0 : return new State(discovered, retTo, contextUrl);
554 : }
555 :
556 : boolean isAllowedOpenID(String id) {
557 99 : for (OpenIdProviderPattern pattern : allowedOpenIDs) {
558 99 : if (pattern.matches(id)) {
559 99 : return true;
560 : }
561 99 : }
562 0 : return false;
563 : }
564 :
565 : private static class State {
566 : final DiscoveryInformation discovered;
567 : final UrlEncoded retTo;
568 : final String contextUrl;
569 :
570 0 : State(DiscoveryInformation d, UrlEncoded r, String c) {
571 0 : discovered = d;
572 0 : retTo = r;
573 0 : contextUrl = c;
574 0 : }
575 : }
576 : }
|