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

com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata Maven / Gradle / Ivy

Go to download

OAuth 2.0 SDK with OpenID Connection extensions for developing client and server applications.

There is a newer version: 11.19.1
Show newest version
/*
 * oauth2-oidc-sdk
 *
 * Copyright 2012-2022 Connect2id Ltd and contributors.
 *
 * 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.
 */

package com.nimbusds.openid.connect.sdk.op;


import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.*;

import net.minidev.json.JSONObject;

import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.langtag.LangTag;
import com.nimbusds.langtag.LangTagException;
import com.nimbusds.oauth2.sdk.GeneralException;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.as.AuthorizationServerEndpointMetadata;
import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.http.HTTPRequestConfigurator;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.id.Identifier;
import com.nimbusds.oauth2.sdk.id.Issuer;
import com.nimbusds.oauth2.sdk.util.CollectionUtils;
import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
import com.nimbusds.openid.connect.sdk.Display;
import com.nimbusds.openid.connect.sdk.SubjectType;
import com.nimbusds.openid.connect.sdk.assurance.IdentityTrustFramework;
import com.nimbusds.openid.connect.sdk.assurance.evidences.*;
import com.nimbusds.openid.connect.sdk.assurance.evidences.attachment.AttachmentType;
import com.nimbusds.openid.connect.sdk.assurance.evidences.attachment.HashAlgorithm;
import com.nimbusds.openid.connect.sdk.claims.ACR;
import com.nimbusds.openid.connect.sdk.claims.ClaimType;
import com.nimbusds.openid.connect.sdk.federation.registration.ClientRegistrationType;


/**
 * OpenID Provider (OP) metadata.
 *
 * 

Related specifications: * *

    *
  • OpenID Connect Discovery 1.0, section 3. *
  • OpenID Connect Session Management 1.0, section 2.1 (draft 28). *
  • OpenID Connect Front-Channel Logout 1.0, section 3 (draft 02). *
  • OpenID Connect Back-Channel Logout 1.0, section 2.1 (draft 07). *
  • OpenID Connect for Identity Assurance 1.0 (draft 12). *
  • OpenID Connect Federation 1.0 (draft 23). *
  • OAuth 2.0 Authorization Server Metadata (RFC 8414) *
  • OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound * Access Tokens (RFC 8705) *
  • Financial-grade API: JWT Secured Authorization Response Mode for * OAuth 2.0 (JARM) *
  • OAuth 2.0 Authorization Server Issuer Identification (RFC 9207) *
  • Initiating User Registration via OpenID Connect (draft 04) *
*/ public class OIDCProviderMetadata extends AuthorizationServerMetadata implements ReadOnlyOIDCProviderMetadata { /** * The registered parameter names. */ private static final Set REGISTERED_PARAMETER_NAMES; static { Set p = new HashSet<>(AuthorizationServerMetadata.getRegisteredParameterNames()); p.addAll(OIDCProviderEndpointMetadata.getRegisteredParameterNames()); p.add("acr_values_supported"); p.add("subject_types_supported"); p.add("id_token_signing_alg_values_supported"); p.add("id_token_encryption_alg_values_supported"); p.add("id_token_encryption_enc_values_supported"); p.add("userinfo_signing_alg_values_supported"); p.add("userinfo_encryption_alg_values_supported"); p.add("userinfo_encryption_enc_values_supported"); p.add("display_values_supported"); p.add("claim_types_supported"); p.add("claims_supported"); p.add("claims_locales_supported"); p.add("claims_parameter_supported"); p.add("backchannel_logout_supported"); p.add("backchannel_logout_session_supported"); p.add("frontchannel_logout_supported"); p.add("frontchannel_logout_session_supported"); p.add("verified_claims_supported"); p.add("trust_frameworks_supported"); p.add("evidence_supported"); p.add("documents_supported"); p.add("documents_methods_supported"); p.add("documents_validation_methods_supported"); p.add("documents_verification_methods_supported"); p.add("id_documents_supported"); // deprecated p.add("id_documents_verification_methods_supported"); // deprecated p.add("electronic_records_supported"); p.add("claims_in_verified_claims_supported"); p.add("attachments_supported"); p.add("digest_algorithms_supported"); REGISTERED_PARAMETER_NAMES = Collections.unmodifiableSet(p); } /** * The UserInfo endpoint. */ private URI userInfoEndpoint; /** * The cross-origin check session iframe. */ private URI checkSessionIframe; /** * The logout endpoint. */ private URI endSessionEndpoint; /** * The supported ACRs. */ private List acrValues; /** * The supported subject types. */ private final List subjectTypes; /** * The supported ID token JWS algorithms. */ private List idTokenJWSAlgs; /** * The supported ID token JWE algorithms. */ private List idTokenJWEAlgs; /** * The supported ID token encryption methods. */ private List idTokenJWEEncs; /** * The supported UserInfo JWS algorithms. */ private List userInfoJWSAlgs; /** * The supported UserInfo JWE algorithms. */ private List userInfoJWEAlgs; /** * The supported UserInfo encryption methods. */ private List userInfoJWEEncs; /** * The supported displays. */ private List displays; /** * The supported claim types. */ private List claimTypes; /** * The supported claims names. */ private List claims; /** * The supported claims locales. */ private List claimsLocales; /** * If {@code true} the {@code claims} parameter is supported, else not. */ private boolean claimsParamSupported = false; /** * If {@code true} the {@code frontchannel_logout_supported} parameter * is set, else not. */ private boolean frontChannelLogoutSupported = false; /** * If {@code true} the {@code frontchannel_logout_session_supported} * parameter is set, else not. */ private boolean frontChannelLogoutSessionSupported = false; /** * If {@code true} the {@code backchannel_logout_supported} parameter * is set, else not. */ private boolean backChannelLogoutSupported = false; /** * If {@code true} the {@code backchannel_logout_session_supported} * parameter is set, else not. */ private boolean backChannelLogoutSessionSupported = false; /** * If {@code true} verified claims are supported. */ private boolean verifiedClaimsSupported = false; /** * The supported trust frameworks. */ private List trustFrameworks; /** * The supported identity evidence types. */ private List evidenceTypes; /** * The supported identity document types. */ private List documentTypes; /** * The supported coarse identity verification methods for evidences of * type document. */ private List documentMethods; /** * The supported validation methods for evidences of type document. */ private List documentValidationMethods; /** * The supported verification methods for evidences of type document. */ private List documentVerificationMethods; /** * The supported identity document types. */ @Deprecated private List idDocumentTypes; /** * The supported verification methods for identity documents. */ @Deprecated private List idVerificationMethods; /** * The supported electronic record types. */ private List electronicRecordTypes; /** * The supported verified claims. */ private List verifiedClaims; /** * The supported attachment types. */ private List attachmentTypes; /** * The supported digest algorithms for external attachments. */ private List attachmentDigestAlgs; /** * Creates a new OpenID Connect provider metadata instance. * * @param issuer The issuer identifier. Must be a URI using the * https scheme with no query or fragment * component. Must not be {@code null}. * @param subjectTypes The supported subject types. At least one must * be specified. Must not be {@code null}. * @param jwkSetURI The JWK set URI. Must not be {@code null}. */ public OIDCProviderMetadata(final Issuer issuer, final List subjectTypes, final URI jwkSetURI) { super(issuer); ensureAtLeastOneSubjectType(subjectTypes); this.subjectTypes = subjectTypes; if (jwkSetURI == null) throw new IllegalArgumentException("The public JWK set URI must not be null"); setJWKSetURI(jwkSetURI); // Default OpenID Connect setting is supported setSupportsRequestURIParam(true); } /** * Creates a new OpenID Connect Federation 1.0 provider metadata * instance. The provider JWK set should be specified by * {@code jwks_uri}, {@code signed_jwks_uri} or {@code jwks}. * * @param issuer The issuer identifier. Must be a URI * using the https scheme with no query * or fragment component. Must not be * {@code null}. * @param subjectTypes The supported subject types. At least * one must be specified. Must not be * {@code null}. * @param clientRegistrationTypes The supported client registration * types. At least one must be * specified. Must not be {@code null}. * @param jwkSetURI The JWK set URI, {@code null} if * specified by another field. * @param signedJWKSetURI The signed JWK set URI, {@code null} * if specified by another field. * @param jwkSet the JWK set, {@code null} if * specified by another field. */ public OIDCProviderMetadata(final Issuer issuer, final List subjectTypes, final List clientRegistrationTypes, final URI jwkSetURI, final URI signedJWKSetURI, final JWKSet jwkSet) { super(issuer); ensureAtLeastOneSubjectType(subjectTypes); this.subjectTypes = subjectTypes; if (clientRegistrationTypes.size() < 1) { throw new IllegalArgumentException("At least one federation client registration type must be specified"); } setClientRegistrationTypes(clientRegistrationTypes); if (jwkSetURI == null && signedJWKSetURI == null && jwkSet == null) { throw new IllegalArgumentException("At least one public JWK must be specified"); } setJWKSetURI(jwkSetURI); setSignedJWKSetURI(signedJWKSetURI); setJWKSet(jwkSet); // Default OpenID Connect setting is supported setSupportsRequestURIParam(true); } private void ensureAtLeastOneSubjectType(final List subjectTypes) { if (subjectTypes.size() < 1) throw new IllegalArgumentException("At least one supported subject type must be specified"); } @Override public void setMtlsEndpointAliases(AuthorizationServerEndpointMetadata mtlsEndpointAliases) { if (mtlsEndpointAliases != null && !(mtlsEndpointAliases instanceof OIDCProviderEndpointMetadata)) { // convert the provided endpoints to OIDC super.setMtlsEndpointAliases(new OIDCProviderEndpointMetadata(mtlsEndpointAliases)); } else { super.setMtlsEndpointAliases(mtlsEndpointAliases); } } @Override public OIDCProviderEndpointMetadata getReadOnlyMtlsEndpointAliases() { return getMtlsEndpointAliases(); } @Override public OIDCProviderEndpointMetadata getMtlsEndpointAliases() { return (OIDCProviderEndpointMetadata) super.getMtlsEndpointAliases(); } /** * Gets the registered OpenID Connect provider metadata parameter * names. * * @return The registered OpenID Connect provider metadata parameter * names, as an unmodifiable set. */ public static Set getRegisteredParameterNames() { return REGISTERED_PARAMETER_NAMES; } @Override public URI getUserInfoEndpointURI() { return userInfoEndpoint; } /** * Sets the UserInfo endpoint URI. Corresponds the * {@code userinfo_endpoint} metadata field. * * @param userInfoEndpoint The UserInfo endpoint URI, {@code null} if * not specified. */ public void setUserInfoEndpointURI(final URI userInfoEndpoint) { this.userInfoEndpoint = userInfoEndpoint; } @Override public URI getCheckSessionIframeURI() { return checkSessionIframe; } /** * Sets the cross-origin check session iframe URI. Corresponds to the * {@code check_session_iframe} metadata field. * * @param checkSessionIframe The check session iframe URI, {@code null} * if not specified. */ public void setCheckSessionIframeURI(final URI checkSessionIframe) { this.checkSessionIframe = checkSessionIframe; } @Override public URI getEndSessionEndpointURI() { return endSessionEndpoint; } /** * Sets the logout endpoint URI. Corresponds to the * {@code end_session_endpoint} metadata field. * * @param endSessionEndpoint The logoout endpoint URI, {@code null} if * not specified. */ public void setEndSessionEndpointURI(final URI endSessionEndpoint) { this.endSessionEndpoint = endSessionEndpoint; } @Override public List getACRs() { return acrValues; } /** * Sets the supported Authentication Context Class References (ACRs). * Corresponds to the {@code acr_values_supported} metadata field. * * @param acrValues The supported ACRs, {@code null} if not specified. */ public void setACRs(final List acrValues) { this.acrValues = acrValues; } @Override public List getSubjectTypes() { return subjectTypes; } @Override public List getIDTokenJWSAlgs() { return idTokenJWSAlgs; } /** * Sets the supported JWS algorithms for ID tokens. Corresponds to the * {@code id_token_signing_alg_values_supported} metadata field. * * @param idTokenJWSAlgs The supported JWS algorithms, {@code null} if * not specified. */ public void setIDTokenJWSAlgs(final List idTokenJWSAlgs) { this.idTokenJWSAlgs = idTokenJWSAlgs; } @Override public List getIDTokenJWEAlgs() { return idTokenJWEAlgs; } /** * Sets the supported JWE algorithms for ID tokens. Corresponds to the * {@code id_token_encryption_alg_values_supported} metadata field. * * @param idTokenJWEAlgs The supported JWE algorithms, {@code null} if * not specified. */ public void setIDTokenJWEAlgs(final List idTokenJWEAlgs) { this.idTokenJWEAlgs = idTokenJWEAlgs; } @Override public List getIDTokenJWEEncs() { return idTokenJWEEncs; } /** * Sets the supported encryption methods for ID tokens. Corresponds to * the {@code id_token_encryption_enc_values_supported} metadata field. * * @param idTokenJWEEncs The supported encryption methods, {@code null} * if not specified. */ public void setIDTokenJWEEncs(final List idTokenJWEEncs) { this.idTokenJWEEncs = idTokenJWEEncs; } @Override public List getUserInfoJWSAlgs() { return userInfoJWSAlgs; } /** * Sets the supported JWS algorithms for UserInfo JWTs. Corresponds to * the {@code userinfo_signing_alg_values_supported} metadata field. * * @param userInfoJWSAlgs The supported JWS algorithms, {@code null} if * not specified. */ public void setUserInfoJWSAlgs(final List userInfoJWSAlgs) { this.userInfoJWSAlgs = userInfoJWSAlgs; } @Override public List getUserInfoJWEAlgs() { return userInfoJWEAlgs; } /** * Sets the supported JWE algorithms for UserInfo JWTs. Corresponds to * the {@code userinfo_encryption_alg_values_supported} metadata field. * * @param userInfoJWEAlgs The supported JWE algorithms, {@code null} if * not specified. */ public void setUserInfoJWEAlgs(final List userInfoJWEAlgs) { this.userInfoJWEAlgs = userInfoJWEAlgs; } @Override public List getUserInfoJWEEncs() { return userInfoJWEEncs; } /** * Sets the supported encryption methods for UserInfo JWTs. Corresponds * to the {@code userinfo_encryption_enc_values_supported} metadata * field. * * @param userInfoJWEEncs The supported encryption methods, * {@code null} if not specified. */ public void setUserInfoJWEEncs(final List userInfoJWEEncs) { this.userInfoJWEEncs = userInfoJWEEncs; } @Override public List getDisplays() { return displays; } /** * Sets the supported displays. Corresponds to the * {@code display_values_supported} metadata field. * * @param displays The supported displays, {@code null} if not * specified. */ public void setDisplays(final List displays) { this.displays = displays; } @Override public List getClaimTypes() { return claimTypes; } /** * Sets the supported claim types. Corresponds to the * {@code claim_types_supported} metadata field. * * @param claimTypes The supported claim types, {@code null} if not * specified. */ public void setClaimTypes(final List claimTypes) { this.claimTypes = claimTypes; } @Override public List getClaims() { return claims; } /** * Sets the supported claims names. Corresponds to the * {@code claims_supported} metadata field. * * @param claims The supported claims names, {@code null} if not * specified. */ public void setClaims(final List claims) { this.claims = claims; } @Override public List getClaimsLocales() { return claimsLocales; } /** * Sets the supported claims locales. Corresponds to the * {@code claims_locales_supported} metadata field. * * @param claimsLocales The supported claims locales, {@code null} if * not specified. */ public void setClaimLocales(final List claimsLocales) { this.claimsLocales = claimsLocales; } @Override public boolean supportsClaimsParam() { return claimsParamSupported; } /** * Sets the support for the {@code claims} authorisation request * parameter. Corresponds to the {@code claims_parameter_supported} * metadata field. * * @param claimsParamSupported {@code true} if the {@code claim} * parameter is supported, else * {@code false}. */ public void setSupportsClaimsParams(final boolean claimsParamSupported) { this.claimsParamSupported = claimsParamSupported; } @Override public boolean supportsFrontChannelLogout() { return frontChannelLogoutSupported; } /** * Sets the support for front-channel logout. Corresponds to the * {@code frontchannel_logout_supported} metadata field. * * @param frontChannelLogoutSupported {@code true} if front-channel * logout is supported, else * {@code false}. */ public void setSupportsFrontChannelLogout(final boolean frontChannelLogoutSupported) { this.frontChannelLogoutSupported = frontChannelLogoutSupported; } @Override public boolean supportsFrontChannelLogoutSession() { return frontChannelLogoutSessionSupported; } /** * Sets the support for front-channel logout with a session ID. * Corresponds to the {@code frontchannel_logout_session_supported} * metadata field. * * @param frontChannelLogoutSessionSupported {@code true} if * front-channel logout with * a session ID is supported, * else {@code false}. */ public void setSupportsFrontChannelLogoutSession(final boolean frontChannelLogoutSessionSupported) { this.frontChannelLogoutSessionSupported = frontChannelLogoutSessionSupported; } @Override public boolean supportsBackChannelLogout() { return backChannelLogoutSupported; } /** * Sets the support for back-channel logout. Corresponds to the * {@code backchannel_logout_supported} metadata field. * * @param backChannelLogoutSupported {@code true} if back-channel * logout is supported, else * {@code false}. */ public void setSupportsBackChannelLogout(final boolean backChannelLogoutSupported) { this.backChannelLogoutSupported = backChannelLogoutSupported; } @Override public boolean supportsBackChannelLogoutSession() { return backChannelLogoutSessionSupported; } /** * Sets the support for back-channel logout with a session ID. * Corresponds to the {@code backchannel_logout_session_supported} * metadata field. * * @param backChannelLogoutSessionSupported {@code true} if * back-channel logout with a * session ID is supported, * else {@code false}. */ public void setSupportsBackChannelLogoutSession(final boolean backChannelLogoutSessionSupported) { this.backChannelLogoutSessionSupported = backChannelLogoutSessionSupported; } @Override public boolean supportsVerifiedClaims() { return verifiedClaimsSupported; } /** * Sets support for verified claims. Corresponds to the * {@code verified_claims_supported} metadata field. * * @param verifiedClaimsSupported {@code true} if verified claims are * supported, else {@code false}. */ public void setSupportsVerifiedClaims(final boolean verifiedClaimsSupported) { this.verifiedClaimsSupported = verifiedClaimsSupported; } @Override public List getIdentityTrustFrameworks() { return trustFrameworks; } /** * Sets the supported identity trust frameworks. Corresponds to the * {@code trust_frameworks_supported} metadata field. * * @param trustFrameworks The supported identity trust frameworks, * {@code null} if not specified. */ public void setIdentityTrustFrameworks(final List trustFrameworks) { this.trustFrameworks = trustFrameworks; } @Override public List getIdentityEvidenceTypes() { return evidenceTypes; } /** * Sets the supported identity evidence types. Corresponds to the * {@code evidence_supported} metadata field. * * @param evidenceTypes The supported identity evidence types, * {@code null} if not specified. */ public void setIdentityEvidenceTypes(final List evidenceTypes) { this.evidenceTypes = evidenceTypes; } @Override public List getDocumentTypes() { return documentTypes; } /** * Sets the supported identity document types. Corresponds to the * {@code documents_supported} metadata field. * * @param documentTypes The supported identity document types, * {@code null} if not specified. */ public void setDocumentTypes(final List documentTypes) { this.documentTypes = documentTypes; } @Override @Deprecated public List getIdentityDocumentTypes() { return idDocumentTypes; } /** * Sets the supported identity document types. Corresponds to the * {@code id_documents_supported} metadata field. * * @param idDocuments The supported identity document types, * {@code null} if not specified. * * @deprecated Use {@link #setDocumentTypes} instead. */ @Deprecated public void setIdentityDocumentTypes(final List idDocuments) { this.idDocumentTypes = idDocuments; } @Override public List getDocumentMethods() { return documentMethods; } /** * Sets the supported coarse identity verification methods for * evidences of type document. Corresponds to the * {@code documents_methods_supported} metadata field. * * @param methods The supported identity verification methods for * document evidences, {@code null} if not specified. */ public void setDocumentMethods(final List methods) { this.documentMethods = methods; } @Override public List getDocumentValidationMethods() { return documentValidationMethods; } /** * Sets the supported validation methods for evidences of type * document. Corresponds to the * {@code documents_validation_methods_supported} metadata field. * * @param methods The validation methods for document evidences, * {@code null} if not specified. */ public void setDocumentValidationMethods(final List methods) { this.documentValidationMethods = methods; } @Override public List getDocumentVerificationMethods() { return documentVerificationMethods; } /** * Sets the supported verification methods for evidences of type * document. Corresponds to the * {@code documents_verification_methods_supported} metadata field. * * @param methods The verification methods for document evidences, * {@code null} if not specified. */ public void setDocumentVerificationMethods(final List methods) { this.documentVerificationMethods = methods; } @Override public List getElectronicRecordTypes() { return electronicRecordTypes; } /** * Sets the supported electronic record types. Corresponds to the * {@code electronic_records_supported} metadata field. * * @param electronicRecordTypes The supported electronic record types, * {@code null} if not specified. */ public void setElectronicRecordTypes(final List electronicRecordTypes) { this.electronicRecordTypes = electronicRecordTypes; } @Override @Deprecated public List getIdentityVerificationMethods() { return idVerificationMethods; } /** * Sets the supported identity verification methods. Corresponds to the * {@code id_documents_verification_methods_supported} metadata field. * * @param idVerificationMethods The supported identity verification * methods, {@code null} if not specified. */ @Deprecated public void setIdentityVerificationMethods(final List idVerificationMethods) { this.idVerificationMethods = idVerificationMethods; } @Override public List getVerifiedClaims() { return verifiedClaims; } /** * Sets the names of the supported verified claims. Corresponds to the * {@code claims_in_verified_claims_supported} metadata field. * * @param verifiedClaims The supported verified claims names, * {@code null} if not specified. */ public void setVerifiedClaims(final List verifiedClaims) { this.verifiedClaims = verifiedClaims; } @Override public List getAttachmentTypes() { return attachmentTypes; } /** * Sets the supported evidence attachment types. Corresponds to the * {@code attachments_supported} metadata field. * * @param attachmentTypes The supported evidence attachment types, * empty if attachments are not supported, * {@code null} if not specified. */ public void setAttachmentTypes(final List attachmentTypes) { this.attachmentTypes = attachmentTypes; } @Override public List getAttachmentDigestAlgs() { return attachmentDigestAlgs; } /** * Sets the supported digest algorithms for the external evidence * attachments. Corresponds to the {@code digest_algorithms_supported} * metadata field. * * @param digestAlgs The supported digest algorithms, {@code null} if * not specified. */ public void setAttachmentDigestAlgs(final List digestAlgs) { this.attachmentDigestAlgs = digestAlgs; } /** * Applies the OpenID Provider metadata defaults where no values have * been specified. * *
    *
  • The response modes default to {@code ["query", "fragment"]}. *
  • The grant types default to {@code ["authorization_code", * "implicit"]}. *
  • The token endpoint authentication methods default to * {@code ["client_secret_basic"]}. *
  • The claim types default to {@code ["normal]}. *
*/ public void applyDefaults() { super.applyDefaults(); if (claimTypes == null) { claimTypes = new ArrayList<>(1); claimTypes.add(ClaimType.NORMAL); } } @Override public JSONObject toJSONObject() { JSONObject o = super.toJSONObject(); // Mandatory fields List stringList = new ArrayList<>(subjectTypes.size()); for (SubjectType st: subjectTypes) stringList.add(st.toString()); o.put("subject_types_supported", stringList); // Optional fields if (userInfoEndpoint != null) o.put("userinfo_endpoint", userInfoEndpoint.toString()); if (checkSessionIframe != null) o.put("check_session_iframe", checkSessionIframe.toString()); if (endSessionEndpoint != null) o.put("end_session_endpoint", endSessionEndpoint.toString()); if (acrValues != null) { o.put("acr_values_supported", Identifier.toStringList(acrValues)); } if (idTokenJWSAlgs != null) { stringList = new ArrayList<>(idTokenJWSAlgs.size()); for (JWSAlgorithm alg: idTokenJWSAlgs) stringList.add(alg.getName()); o.put("id_token_signing_alg_values_supported", stringList); } if (idTokenJWEAlgs != null) { stringList = new ArrayList<>(idTokenJWEAlgs.size()); for (JWEAlgorithm alg: idTokenJWEAlgs) stringList.add(alg.getName()); o.put("id_token_encryption_alg_values_supported", stringList); } if (idTokenJWEEncs != null) { stringList = new ArrayList<>(idTokenJWEEncs.size()); for (EncryptionMethod m: idTokenJWEEncs) stringList.add(m.getName()); o.put("id_token_encryption_enc_values_supported", stringList); } if (userInfoJWSAlgs != null) { stringList = new ArrayList<>(userInfoJWSAlgs.size()); for (JWSAlgorithm alg: userInfoJWSAlgs) stringList.add(alg.getName()); o.put("userinfo_signing_alg_values_supported", stringList); } if (userInfoJWEAlgs != null) { stringList = new ArrayList<>(userInfoJWEAlgs.size()); for (JWEAlgorithm alg: userInfoJWEAlgs) stringList.add(alg.getName()); o.put("userinfo_encryption_alg_values_supported", stringList); } if (userInfoJWEEncs != null) { stringList = new ArrayList<>(userInfoJWEEncs.size()); for (EncryptionMethod m: userInfoJWEEncs) stringList.add(m.getName()); o.put("userinfo_encryption_enc_values_supported", stringList); } if (displays != null) { stringList = new ArrayList<>(displays.size()); for (Display d: displays) stringList.add(d.toString()); o.put("display_values_supported", stringList); } if (claimTypes != null) { stringList = new ArrayList<>(claimTypes.size()); for (ClaimType ct: claimTypes) stringList.add(ct.toString()); o.put("claim_types_supported", stringList); } if (claims != null) o.put("claims_supported", claims); if (claimsLocales != null) { stringList = new ArrayList<>(claimsLocales.size()); for (LangTag l: claimsLocales) stringList.add(l.toString()); o.put("claims_locales_supported", stringList); } if (claimsParamSupported) { o.put("claims_parameter_supported", true); } // Always output, for OP metadata default value is true, for // AS metadata implied default is false o.put("request_uri_parameter_supported", supportsRequestURIParam()); // optional front and back-channel logout if (frontChannelLogoutSupported) { o.put("frontchannel_logout_supported", true); } if (frontChannelLogoutSupported) { o.put("frontchannel_logout_session_supported", frontChannelLogoutSessionSupported); } if (backChannelLogoutSupported) { o.put("backchannel_logout_supported", true); } if (backChannelLogoutSupported) { o.put("backchannel_logout_session_supported", backChannelLogoutSessionSupported); } // OpenID Connect for Identity Assurance 1.0 if (verifiedClaimsSupported) { o.put("verified_claims_supported", true); if (trustFrameworks != null) { o.put("trust_frameworks_supported", Identifier.toStringList(trustFrameworks)); } if (evidenceTypes != null) { o.put("evidence_supported", Identifier.toStringList(evidenceTypes)); } if ( (CollectionUtils.contains(evidenceTypes, IdentityEvidenceType.DOCUMENT) || CollectionUtils.contains(evidenceTypes, IdentityEvidenceType.ID_DOCUMENT)) && documentTypes != null) { o.put("documents_supported", Identifier.toStringList(documentTypes)); // TODO await resolution of // https://bitbucket.org/openid/ekyc-ida/issues/1275/clarification-regarding-op-metadata if (documentMethods != null) { o.put("documents_methods_supported", Identifier.toStringList(documentMethods)); } if (documentValidationMethods != null) { o.put("documents_validation_methods_supported", Identifier.toStringList(documentValidationMethods)); } if (documentVerificationMethods != null) { o.put("documents_verification_methods_supported", Identifier.toStringList(documentVerificationMethods)); } } if (idDocumentTypes != null) { // deprecated o.put("id_documents_supported", Identifier.toStringList(idDocumentTypes)); } if (idVerificationMethods != null) { // deprecated o.put("id_documents_verification_methods_supported", Identifier.toStringList(idVerificationMethods)); } if (electronicRecordTypes != null) { o.put("electronic_records_supported", Identifier.toStringList(electronicRecordTypes)); } if (verifiedClaims != null) { o.put("claims_in_verified_claims_supported", verifiedClaims); } if (attachmentTypes != null) { List strings = new LinkedList<>(); for (AttachmentType type: attachmentTypes) { strings.add(type.toString()); } o.put("attachments_supported", strings); if (attachmentTypes.contains(AttachmentType.EXTERNAL) && attachmentDigestAlgs != null) { o.put("digest_algorithms_supported", Identifier.toStringList(attachmentDigestAlgs)); } } } return o; } /** * Parses an OpenID Provider metadata from the specified JSON object. * * @param jsonObject The JSON object to parse. Must not be * {@code null}. * * @return The OpenID Provider metadata. * * @throws ParseException If the JSON object couldn't be parsed to an * OpenID Provider metadata. */ public static OIDCProviderMetadata parse(final JSONObject jsonObject) throws ParseException { AuthorizationServerMetadata as = AuthorizationServerMetadata.parse(jsonObject); List subjectTypes = new ArrayList<>(); for (String v: JSONObjectUtils.getStringArray(jsonObject, "subject_types_supported")) { subjectTypes.add(SubjectType.parse(v)); } OIDCProviderMetadata op; if (jsonObject.get("client_registration_types_supported") != null) { // OIDC Federation 1.0 constructor List clientRegistrationTypes = new LinkedList<>(); for (String v: JSONObjectUtils.getStringList(jsonObject, "client_registration_types_supported")) { clientRegistrationTypes.add(new ClientRegistrationType(v)); } try { JWKSet jwkSet = null; if (jsonObject.get("jwks") != null) { jwkSet = JWKSet.parse(JSONObjectUtils.getJSONObject(jsonObject, "jwks")); } op = new OIDCProviderMetadata( as.getIssuer(), Collections.unmodifiableList(subjectTypes), clientRegistrationTypes, as.getJWKSetURI(), JSONObjectUtils.getURI(jsonObject, "signed_jwks_uri", null), jwkSet); } catch (java.text.ParseException | IllegalArgumentException e) { throw new ParseException(e.getMessage(), e); } } else { // Regular constructor op = new OIDCProviderMetadata( as.getIssuer(), Collections.unmodifiableList(subjectTypes), as.getJWKSetURI()); } // Endpoints op.setAuthorizationEndpointURI(as.getAuthorizationEndpointURI()); op.setTokenEndpointURI(as.getTokenEndpointURI()); op.setRegistrationEndpointURI(as.getRegistrationEndpointURI()); op.setIntrospectionEndpointURI(as.getIntrospectionEndpointURI()); op.setRevocationEndpointURI(as.getRevocationEndpointURI()); op.setRequestObjectEndpoint(as.getRequestObjectEndpoint()); op.setPushedAuthorizationRequestEndpointURI(as.getPushedAuthorizationRequestEndpointURI()); op.setDeviceAuthorizationEndpointURI(as.getDeviceAuthorizationEndpointURI()); op.userInfoEndpoint = JSONObjectUtils.getURI(jsonObject, "userinfo_endpoint", null); op.checkSessionIframe = JSONObjectUtils.getURI(jsonObject, "check_session_iframe", null); op.endSessionEndpoint = JSONObjectUtils.getURI(jsonObject, "end_session_endpoint", null); // Capabilities op.setScopes(as.getScopes()); op.setResponseTypes(as.getResponseTypes()); op.setResponseModes(as.getResponseModes()); op.setGrantTypes(as.getGrantTypes()); op.setTokenEndpointAuthMethods(as.getTokenEndpointAuthMethods()); op.setTokenEndpointJWSAlgs(as.getTokenEndpointJWSAlgs()); op.setIntrospectionEndpointAuthMethods(as.getIntrospectionEndpointAuthMethods()); op.setIntrospectionEndpointJWSAlgs(as.getIntrospectionEndpointJWSAlgs()); op.setRevocationEndpointAuthMethods(as.getRevocationEndpointAuthMethods()); op.setRevocationEndpointJWSAlgs(as.getRevocationEndpointJWSAlgs()); op.setRequestObjectJWSAlgs(as.getRequestObjectJWSAlgs()); op.setRequestObjectJWEAlgs(as.getRequestObjectJWEAlgs()); op.setRequestObjectJWEEncs(as.getRequestObjectJWEEncs()); op.setSupportsRequestParam(as.supportsRequestParam()); op.setSupportsRequestURIParam(as.supportsRequestURIParam()); op.setRequiresRequestURIRegistration(as.requiresRequestURIRegistration()); op.requiresPushedAuthorizationRequests(as.requiresPushedAuthorizationRequests()); op.setSupportsAuthorizationResponseIssuerParam(as.supportsAuthorizationResponseIssuerParam()); op.setCodeChallengeMethods(as.getCodeChallengeMethods()); op.setBackChannelAuthenticationEndpointURI(as.getBackChannelAuthenticationEndpointURI()); op.setBackChannelAuthenticationRequestJWSAlgs(as.getBackChannelAuthenticationRequestJWSAlgs()); op.setSupportsBackChannelUserCodeParam(as.supportsBackChannelUserCodeParam()); op.setBackChannelTokenDeliveryModes(as.getBackChannelTokenDeliveryModes()); op.setPromptTypes(as.getPromptTypes()); op.setOrganizationName(as.getOrganizationName()); op.setJWKSet(as.getJWKSet()); op.setSignedJWKSetURI(as.getSignedJWKSetURI()); op.setClientRegistrationTypes(as.getClientRegistrationTypes()); op.setClientRegistrationAuthnMethods(as.getClientRegistrationAuthnMethods()); op.setClientRegistrationAuthnJWSAlgs(as.getClientRegistrationAuthnJWSAlgs()); op.setFederationRegistrationEndpointURI(as.getFederationRegistrationEndpointURI()); if (jsonObject.get("acr_values_supported") != null) { op.acrValues = new ArrayList<>(); for (String v: JSONObjectUtils.getStringArray(jsonObject, "acr_values_supported")) { if (v != null) op.acrValues.add(new ACR(v)); } } // ID token if (jsonObject.get("id_token_signing_alg_values_supported") != null) { op.idTokenJWSAlgs = new ArrayList<>(); for (String v: JSONObjectUtils.getStringArray(jsonObject, "id_token_signing_alg_values_supported")) { if (v != null) op.idTokenJWSAlgs.add(JWSAlgorithm.parse(v)); } } if (jsonObject.get("id_token_encryption_alg_values_supported") != null) { op.idTokenJWEAlgs = new ArrayList<>(); for (String v: JSONObjectUtils.getStringArray(jsonObject, "id_token_encryption_alg_values_supported")) { if (v != null) op.idTokenJWEAlgs.add(JWEAlgorithm.parse(v)); } } if (jsonObject.get("id_token_encryption_enc_values_supported") != null) { op.idTokenJWEEncs = new ArrayList<>(); for (String v: JSONObjectUtils.getStringArray(jsonObject, "id_token_encryption_enc_values_supported")) { if (v != null) op.idTokenJWEEncs.add(EncryptionMethod.parse(v)); } } // UserInfo if (jsonObject.get("userinfo_signing_alg_values_supported") != null) { op.userInfoJWSAlgs = new ArrayList<>(); for (String v: JSONObjectUtils.getStringArray(jsonObject, "userinfo_signing_alg_values_supported")) { if (v != null) op.userInfoJWSAlgs.add(JWSAlgorithm.parse(v)); } } if (jsonObject.get("userinfo_encryption_alg_values_supported") != null) { op.userInfoJWEAlgs = new ArrayList<>(); for (String v: JSONObjectUtils.getStringArray(jsonObject, "userinfo_encryption_alg_values_supported")) { if (v != null) op.userInfoJWEAlgs.add(JWEAlgorithm.parse(v)); } } if (jsonObject.get("userinfo_encryption_enc_values_supported") != null) { op.userInfoJWEEncs = new ArrayList<>(); for (String v: JSONObjectUtils.getStringArray(jsonObject, "userinfo_encryption_enc_values_supported")) { if (v != null) op.userInfoJWEEncs.add(EncryptionMethod.parse(v)); } } // Misc if (jsonObject.get("display_values_supported") != null) { op.displays = new ArrayList<>(); for (String v: JSONObjectUtils.getStringArray(jsonObject, "display_values_supported")) { if (v != null) op.displays.add(Display.parse(v)); } } if (jsonObject.get("claim_types_supported") != null) { op.claimTypes = new ArrayList<>(); for (String v: JSONObjectUtils.getStringArray(jsonObject, "claim_types_supported")) { if (v != null) op.claimTypes.add(ClaimType.parse(v)); } } if (jsonObject.get("claims_supported") != null) { op.claims = new ArrayList<>(); for (String v: JSONObjectUtils.getStringArray(jsonObject, "claims_supported")) { if (v != null) op.claims.add(v); } } if (jsonObject.get("claims_locales_supported") != null) { op.claimsLocales = new ArrayList<>(); for (String v : JSONObjectUtils.getStringArray(jsonObject, "claims_locales_supported")) { if (v != null) { try { op.claimsLocales.add(LangTag.parse(v)); } catch (LangTagException e) { throw new ParseException("Invalid claims_locales_supported field: " + e.getMessage(), e); } } } } op.setUILocales(as.getUILocales()); op.setServiceDocsURI(as.getServiceDocsURI()); op.setPolicyURI(as.getPolicyURI()); op.setTermsOfServiceURI(as.getTermsOfServiceURI()); if (jsonObject.get("claims_parameter_supported") != null) op.claimsParamSupported = JSONObjectUtils.getBoolean(jsonObject, "claims_parameter_supported"); if (jsonObject.get("request_uri_parameter_supported") == null) { op.setSupportsRequestURIParam(true); } // Optional front and back-channel logout if (jsonObject.get("frontchannel_logout_supported") != null) op.frontChannelLogoutSupported = JSONObjectUtils.getBoolean(jsonObject, "frontchannel_logout_supported"); if (op.frontChannelLogoutSupported && jsonObject.get("frontchannel_logout_session_supported") != null) op.frontChannelLogoutSessionSupported = JSONObjectUtils.getBoolean(jsonObject, "frontchannel_logout_session_supported"); if (jsonObject.get("backchannel_logout_supported") != null) op.backChannelLogoutSupported = JSONObjectUtils.getBoolean(jsonObject, "backchannel_logout_supported"); if (op.backChannelLogoutSupported && jsonObject.get("backchannel_logout_session_supported") != null) op.backChannelLogoutSessionSupported = JSONObjectUtils.getBoolean(jsonObject, "backchannel_logout_session_supported"); if (jsonObject.get("mtls_endpoint_aliases") != null) op.setMtlsEndpointAliases(OIDCProviderEndpointMetadata.parse(JSONObjectUtils.getJSONObject(jsonObject, "mtls_endpoint_aliases"))); op.setSupportsTLSClientCertificateBoundAccessTokens(as.supportsTLSClientCertificateBoundAccessTokens()); // DPoP op.setDPoPJWSAlgs(as.getDPoPJWSAlgs()); // JARM op.setAuthorizationJWSAlgs(as.getAuthorizationJWSAlgs()); op.setAuthorizationJWEAlgs(as.getAuthorizationJWEAlgs()); op.setAuthorizationJWEEncs(as.getAuthorizationJWEEncs()); // Incremental authz op.setIncrementalAuthorizationTypes(as.getIncrementalAuthorizationTypes()); // OpenID Connect for Identity Assurance 1.0 if (jsonObject.get("verified_claims_supported") != null) { op.verifiedClaimsSupported = JSONObjectUtils.getBoolean(jsonObject, "verified_claims_supported"); if (op.verifiedClaimsSupported) { if (jsonObject.get("trust_frameworks_supported") != null) { op.trustFrameworks = new LinkedList<>(); for (String v : JSONObjectUtils.getStringList(jsonObject, "trust_frameworks_supported")) { op.trustFrameworks.add(new IdentityTrustFramework(v)); } } if (jsonObject.get("evidence_supported") != null) { op.evidenceTypes = new LinkedList<>(); for (String v: JSONObjectUtils.getStringList(jsonObject, "evidence_supported")) { op.evidenceTypes.add(new IdentityEvidenceType(v)); } } if ( (CollectionUtils.contains(op.evidenceTypes, IdentityEvidenceType.DOCUMENT) || CollectionUtils.contains(op.evidenceTypes, IdentityEvidenceType.ID_DOCUMENT)) && jsonObject.get("documents_supported") != null) { op.documentTypes = new LinkedList<>(); for (String v: JSONObjectUtils.getStringList(jsonObject, "documents_supported")) { op.documentTypes.add(new DocumentType(v)); } // TODO await resolution of // https://bitbucket.org/openid/ekyc-ida/issues/1275/clarification-regarding-op-metadata if (jsonObject.get("documents_methods_supported") != null) { op.documentMethods = new LinkedList<>(); for (String v: JSONObjectUtils.getStringList(jsonObject, "documents_methods_supported")) { op.documentMethods.add(new IdentityVerificationMethod(v)); } } if (jsonObject.get("documents_validation_methods_supported") != null) { op.documentValidationMethods = new LinkedList<>(); for (String v: JSONObjectUtils.getStringList(jsonObject, "documents_validation_methods_supported")) { op.documentValidationMethods.add(new ValidationMethodType(v)); } } if (jsonObject.get("documents_verification_methods_supported") != null) { op.documentVerificationMethods = new LinkedList<>(); for (String v: JSONObjectUtils.getStringList(jsonObject, "documents_verification_methods_supported")) { op.documentVerificationMethods.add(new VerificationMethodType(v)); } } } if (jsonObject.get("id_documents_supported") != null) { // deprecated op.idDocumentTypes = new LinkedList<>(); for (String v: JSONObjectUtils.getStringList(jsonObject, "id_documents_supported")) { op.idDocumentTypes.add(new IDDocumentType(v)); } } if (jsonObject.get("id_documents_verification_methods_supported") != null) { // deprecated op.idVerificationMethods = new LinkedList<>(); for (String v: JSONObjectUtils.getStringList(jsonObject, "id_documents_verification_methods_supported")) { op.idVerificationMethods.add(new IdentityVerificationMethod(v)); } } if (jsonObject.get("electronic_records_supported") != null) { op.electronicRecordTypes = new LinkedList<>(); for (String v: JSONObjectUtils.getStringList(jsonObject, "electronic_records_supported")) { op.electronicRecordTypes.add(new ElectronicRecordType(v)); } } if (jsonObject.get("claims_in_verified_claims_supported") != null) { op.verifiedClaims = JSONObjectUtils.getStringList(jsonObject, "claims_in_verified_claims_supported"); } if (jsonObject.get("attachments_supported") != null) { op.attachmentTypes = new LinkedList<>(); for (String v: JSONObjectUtils.getStringList(jsonObject, "attachments_supported")) { op.attachmentTypes.add(AttachmentType.parse(v)); } if (op.attachmentTypes.contains(AttachmentType.EXTERNAL) && jsonObject.get("digest_algorithms_supported") != null) { op.attachmentDigestAlgs = new LinkedList<>(); for (String v: JSONObjectUtils.getStringList(jsonObject, "digest_algorithms_supported")) { op.attachmentDigestAlgs.add(new HashAlgorithm(v)); } } } } } // Parse custom (not registered) parameters for (Map.Entry entry: as.getCustomParameters().entrySet()) { if (REGISTERED_PARAMETER_NAMES.contains(entry.getKey())) continue; // skip op.setCustomParameter(entry.getKey(), entry.getValue()); } return op; } /** * Parses an OpenID Provider metadata from the specified JSON object * string. * * @param s The JSON object sting to parse. Must not be {@code null}. * * @return The OpenID Provider metadata. * * @throws ParseException If the JSON object string couldn't be parsed * to an OpenID Provider metadata. */ public static OIDCProviderMetadata parse(final String s) throws ParseException { return parse(JSONObjectUtils.parse(s)); } /** * Resolves OpenID Provider metadata URL from the specified issuer * identifier. * * @param issuer The OpenID Provider issuer identifier. Must represent * a valid HTTPS or HTTP URL. Must not be {@code null}. * * @return The OpenID Provider metadata URL. * * @throws GeneralException If the issuer identifier is invalid. */ public static URL resolveURL(final Issuer issuer) throws GeneralException { try { URL issuerURL = new URL(issuer.getValue()); // Validate but don't insist on HTTPS, see // http://openid.net/specs/openid-connect-core-1_0.html#Terminology if (issuerURL.getQuery() != null && ! issuerURL.getQuery().trim().isEmpty()) { throw new GeneralException("The issuer identifier must not contain a query component"); } if (issuerURL.getPath() != null && issuerURL.getPath().endsWith("/")) { return new URL(issuerURL + ".well-known/openid-configuration"); } else { return new URL(issuerURL + "/.well-known/openid-configuration"); } } catch (MalformedURLException e) { throw new GeneralException("The issuer identifier doesn't represent a valid URL: " + e.getMessage(), e); } } /** * Resolves OpenID Provider metadata from the specified issuer * identifier. The metadata is downloaded by HTTP GET from * {@code [issuer-url]/.well-known/openid-configuration}. * * @param issuer The OpenID Provider issuer identifier. Must represent * a valid HTTPS or HTTP URL. Must not be {@code null}. * * @return The OpenID Provider metadata. * * @throws GeneralException If the issuer identifier or the downloaded * metadata are invalid. * @throws IOException On a HTTP exception. */ public static OIDCProviderMetadata resolve(final Issuer issuer) throws GeneralException, IOException { return resolve(issuer, 0, 0); } /** * Resolves OpenID Provider metadata from the specified issuer * identifier. The metadata is downloaded by HTTP GET from * {@code [issuer-url]/.well-known/openid-configuration}, using the * specified HTTP timeouts. * * @param issuer The issuer identifier. Must represent a valid * HTTPS or HTTP URL. Must not be {@code null}. * @param connectTimeout The HTTP connect timeout, in milliseconds. * Zero implies no timeout. Must not be negative. * @param readTimeout The HTTP response read timeout, in * milliseconds. Zero implies no timeout. Must * not be negative. * * @return The OpenID Provider metadata. * * @throws GeneralException If the issuer identifier or the downloaded * metadata are invalid. * @throws IOException On an HTTP exception. */ public static OIDCProviderMetadata resolve(final Issuer issuer, final int connectTimeout, final int readTimeout) throws GeneralException, IOException { HTTPRequestConfigurator requestConfigurator = new HTTPRequestConfigurator() { @Override public void configure(HTTPRequest httpRequest) { httpRequest.setConnectTimeout(connectTimeout); httpRequest.setReadTimeout(readTimeout); } }; return resolve(issuer, requestConfigurator); } /** * Resolves OpenID Provider metadata from the specified issuer * identifier. The metadata is downloaded by HTTP GET from * {@code [issuer-url]/.well-known/openid-configuration}, using the * specified HTTP request configurator. * * @param issuer The issuer identifier. Must represent a * valid HTTPS or HTTP URL. Must not be * {@code null}. * @param requestConfigurator An {@link HTTPRequestConfigurator} * instance to perform additional * {@link HTTPRequest} configuration to * fetch the OpenID Provider metadata. Must * not be {@code null}. * * @return The OpenID Provider metadata. * * @throws GeneralException If the issuer identifier or the downloaded * metadata are invalid. * @throws IOException On an HTTP exception. */ public static OIDCProviderMetadata resolve(final Issuer issuer, final HTTPRequestConfigurator requestConfigurator) throws GeneralException, IOException { URL configURL = resolveURL(issuer); HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.GET, configURL); requestConfigurator.configure(httpRequest); HTTPResponse httpResponse = httpRequest.send(); if (httpResponse.getStatusCode() != 200) { throw new IOException("Couldn't download OpenID Provider metadata from " + configURL + ": Status code " + httpResponse.getStatusCode()); } JSONObject jsonObject = httpResponse.getContentAsJSONObject(); OIDCProviderMetadata op = OIDCProviderMetadata.parse(jsonObject); if (! issuer.equals(op.getIssuer())) { throw new GeneralException("The returned issuer doesn't match the expected: " + op.getIssuer()); } return op; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy