[KEYCLOAK-884] - OpenID Connect UserInfo Endpoint.
This commit is contained in:
parent
e4b7dd2df3
commit
4f432775ed
17 changed files with 787 additions and 358 deletions
|
@ -1,96 +1,23 @@
|
||||||
package org.keycloak.representations;
|
package org.keycloak.representations;
|
||||||
|
|
||||||
import org.codehaus.jackson.annotate.JsonProperty;
|
import org.codehaus.jackson.annotate.JsonProperty;
|
||||||
|
import org.codehaus.jackson.annotate.JsonUnwrapped;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class IDToken extends JsonWebToken {
|
public class IDToken extends JsonWebToken {
|
||||||
|
|
||||||
@JsonProperty("nonce")
|
@JsonProperty("nonce")
|
||||||
protected String nonce;
|
protected String nonce;
|
||||||
|
|
||||||
@JsonProperty("name")
|
|
||||||
protected String name;
|
|
||||||
|
|
||||||
@JsonProperty("given_name")
|
|
||||||
protected String givenName;
|
|
||||||
|
|
||||||
@JsonProperty("family_name")
|
|
||||||
protected String familyName;
|
|
||||||
|
|
||||||
@JsonProperty("middle_name")
|
|
||||||
protected String middleName;
|
|
||||||
|
|
||||||
@JsonProperty("nickname")
|
|
||||||
protected String nickName;
|
|
||||||
|
|
||||||
@JsonProperty("preferred_username")
|
|
||||||
protected String preferredUsername;
|
|
||||||
|
|
||||||
@JsonProperty("profile")
|
|
||||||
protected String profile;
|
|
||||||
|
|
||||||
@JsonProperty("picture")
|
|
||||||
protected String picture;
|
|
||||||
|
|
||||||
@JsonProperty("website")
|
|
||||||
protected String website;
|
|
||||||
|
|
||||||
@JsonProperty("email")
|
|
||||||
protected String email;
|
|
||||||
|
|
||||||
@JsonProperty("email_verified")
|
|
||||||
protected Boolean emailVerified;
|
|
||||||
|
|
||||||
@JsonProperty("gender")
|
|
||||||
protected String gender;
|
|
||||||
|
|
||||||
@JsonProperty("birthdate")
|
|
||||||
protected String birthdate;
|
|
||||||
|
|
||||||
@JsonProperty("zoneinfo")
|
|
||||||
protected String zoneinfo;
|
|
||||||
|
|
||||||
@JsonProperty("locale")
|
|
||||||
protected String locale;
|
|
||||||
|
|
||||||
@JsonProperty("phone_number")
|
|
||||||
protected String phoneNumber;
|
|
||||||
|
|
||||||
@JsonProperty("phone_number_verified")
|
|
||||||
protected Boolean phoneNumberVerified;
|
|
||||||
|
|
||||||
@JsonProperty("address")
|
|
||||||
protected String address;
|
|
||||||
|
|
||||||
@JsonProperty("updated_at")
|
|
||||||
protected Long updatedAt;
|
|
||||||
|
|
||||||
@JsonProperty("formatted")
|
|
||||||
protected String formattedAddress;
|
|
||||||
|
|
||||||
@JsonProperty("street_address")
|
|
||||||
protected String streetAddress;
|
|
||||||
|
|
||||||
@JsonProperty("locality")
|
|
||||||
protected String locality;
|
|
||||||
|
|
||||||
@JsonProperty("region")
|
|
||||||
protected String region;
|
|
||||||
|
|
||||||
@JsonProperty("postal_code")
|
|
||||||
protected String postalCode;
|
|
||||||
|
|
||||||
@JsonProperty("country")
|
|
||||||
protected String country;
|
|
||||||
|
|
||||||
@JsonProperty("claims_locales")
|
|
||||||
protected String claimsLocales;
|
|
||||||
|
|
||||||
@JsonProperty("session_state")
|
@JsonProperty("session_state")
|
||||||
protected String sessionState;
|
protected String sessionState;
|
||||||
|
|
||||||
|
@JsonUnwrapped
|
||||||
|
protected UserClaimSet userClaimSet = new UserClaimSet();
|
||||||
|
|
||||||
public String getNonce() {
|
public String getNonce() {
|
||||||
return nonce;
|
return nonce;
|
||||||
}
|
}
|
||||||
|
@ -99,214 +26,6 @@ public class IDToken extends JsonWebToken {
|
||||||
this.nonce = nonce;
|
this.nonce = nonce;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getGivenName() {
|
|
||||||
return givenName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGivenName(String givenName) {
|
|
||||||
this.givenName = givenName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFamilyName() {
|
|
||||||
return familyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFamilyName(String familyName) {
|
|
||||||
this.familyName = familyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMiddleName() {
|
|
||||||
return middleName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMiddleName(String middleName) {
|
|
||||||
this.middleName = middleName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getNickName() {
|
|
||||||
return nickName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setNickName(String nickName) {
|
|
||||||
this.nickName = nickName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPreferredUsername() {
|
|
||||||
return preferredUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPreferredUsername(String preferredUsername) {
|
|
||||||
this.preferredUsername = preferredUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getProfile() {
|
|
||||||
return profile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProfile(String profile) {
|
|
||||||
this.profile = profile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPicture() {
|
|
||||||
return picture;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPicture(String picture) {
|
|
||||||
this.picture = picture;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getWebsite() {
|
|
||||||
return website;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setWebsite(String website) {
|
|
||||||
this.website = website;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getEmail() {
|
|
||||||
return email;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEmail(String email) {
|
|
||||||
this.email = email;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean getEmailVerified() {
|
|
||||||
return emailVerified;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEmailVerified(Boolean emailVerified) {
|
|
||||||
this.emailVerified = emailVerified;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getGender() {
|
|
||||||
return gender;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGender(String gender) {
|
|
||||||
this.gender = gender;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getBirthdate() {
|
|
||||||
return birthdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBirthdate(String birthdate) {
|
|
||||||
this.birthdate = birthdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getZoneinfo() {
|
|
||||||
return zoneinfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setZoneinfo(String zoneinfo) {
|
|
||||||
this.zoneinfo = zoneinfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLocale() {
|
|
||||||
return locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLocale(String locale) {
|
|
||||||
this.locale = locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPhoneNumber() {
|
|
||||||
return phoneNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPhoneNumber(String phoneNumber) {
|
|
||||||
this.phoneNumber = phoneNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean getPhoneNumberVerified() {
|
|
||||||
return phoneNumberVerified;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPhoneNumberVerified(Boolean phoneNumberVerified) {
|
|
||||||
this.phoneNumberVerified = phoneNumberVerified;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAddress() {
|
|
||||||
return address;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAddress(String address) {
|
|
||||||
this.address = address;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getUpdatedAt() {
|
|
||||||
return updatedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUpdatedAt(Long updatedAt) {
|
|
||||||
this.updatedAt = updatedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFormattedAddress() {
|
|
||||||
return formattedAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFormattedAddress(String formattedAddress) {
|
|
||||||
this.formattedAddress = formattedAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getStreetAddress() {
|
|
||||||
return streetAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStreetAddress(String streetAddress) {
|
|
||||||
this.streetAddress = streetAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLocality() {
|
|
||||||
return locality;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLocality(String locality) {
|
|
||||||
this.locality = locality;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRegion() {
|
|
||||||
return region;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRegion(String region) {
|
|
||||||
this.region = region;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPostalCode() {
|
|
||||||
return postalCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPostalCode(String postalCode) {
|
|
||||||
this.postalCode = postalCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCountry() {
|
|
||||||
return country;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCountry(String country) {
|
|
||||||
this.country = country;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getClaimsLocales() {
|
|
||||||
return claimsLocales;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setClaimsLocales(String claimsLocales) {
|
|
||||||
this.claimsLocales = claimsLocales;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSessionState() {
|
public String getSessionState() {
|
||||||
return sessionState;
|
return sessionState;
|
||||||
}
|
}
|
||||||
|
@ -314,4 +33,12 @@ public class IDToken extends JsonWebToken {
|
||||||
public void setSessionState(String sessionState) {
|
public void setSessionState(String sessionState) {
|
||||||
this.sessionState = sessionState;
|
this.sessionState = sessionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UserClaimSet getUserClaimSet() {
|
||||||
|
return this.userClaimSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserClaimSet(UserClaimSet userClaimSet) {
|
||||||
|
this.userClaimSet = userClaimSet;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,6 +127,10 @@ public class JsonWebToken implements Serializable {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSubject(String subject) {
|
||||||
|
this.subject = subject;
|
||||||
|
}
|
||||||
|
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,331 @@
|
||||||
|
/*
|
||||||
|
* JBoss, Home of Professional Open Source
|
||||||
|
*
|
||||||
|
* Copyright 2013 Red Hat, Inc. and/or its affiliates.
|
||||||
|
*
|
||||||
|
* 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 org.keycloak.representations;
|
||||||
|
|
||||||
|
import org.codehaus.jackson.annotate.JsonProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author pedroigor
|
||||||
|
*/
|
||||||
|
public class UserClaimSet {
|
||||||
|
|
||||||
|
@JsonProperty("sub")
|
||||||
|
protected String sub;
|
||||||
|
|
||||||
|
@JsonProperty("name")
|
||||||
|
protected String name;
|
||||||
|
|
||||||
|
@JsonProperty("given_name")
|
||||||
|
protected String givenName;
|
||||||
|
|
||||||
|
@JsonProperty("family_name")
|
||||||
|
protected String familyName;
|
||||||
|
|
||||||
|
@JsonProperty("middle_name")
|
||||||
|
protected String middleName;
|
||||||
|
|
||||||
|
@JsonProperty("nickname")
|
||||||
|
protected String nickName;
|
||||||
|
|
||||||
|
@JsonProperty("preferred_username")
|
||||||
|
protected String preferredUsername;
|
||||||
|
|
||||||
|
@JsonProperty("profile")
|
||||||
|
protected String profile;
|
||||||
|
|
||||||
|
@JsonProperty("picture")
|
||||||
|
protected String picture;
|
||||||
|
|
||||||
|
@JsonProperty("website")
|
||||||
|
protected String website;
|
||||||
|
|
||||||
|
@JsonProperty("email")
|
||||||
|
protected String email;
|
||||||
|
|
||||||
|
@JsonProperty("email_verified")
|
||||||
|
protected Boolean emailVerified;
|
||||||
|
|
||||||
|
@JsonProperty("gender")
|
||||||
|
protected String gender;
|
||||||
|
|
||||||
|
@JsonProperty("birthdate")
|
||||||
|
protected String birthdate;
|
||||||
|
|
||||||
|
@JsonProperty("zoneinfo")
|
||||||
|
protected String zoneinfo;
|
||||||
|
|
||||||
|
@JsonProperty("locale")
|
||||||
|
protected String locale;
|
||||||
|
|
||||||
|
@JsonProperty("phone_number")
|
||||||
|
protected String phoneNumber;
|
||||||
|
|
||||||
|
@JsonProperty("phone_number_verified")
|
||||||
|
protected Boolean phoneNumberVerified;
|
||||||
|
|
||||||
|
@JsonProperty("address")
|
||||||
|
protected String address;
|
||||||
|
|
||||||
|
@JsonProperty("updated_at")
|
||||||
|
protected Long updatedAt;
|
||||||
|
|
||||||
|
@JsonProperty("formatted")
|
||||||
|
protected String formattedAddress;
|
||||||
|
|
||||||
|
@JsonProperty("street_address")
|
||||||
|
protected String streetAddress;
|
||||||
|
|
||||||
|
@JsonProperty("locality")
|
||||||
|
protected String locality;
|
||||||
|
|
||||||
|
@JsonProperty("region")
|
||||||
|
protected String region;
|
||||||
|
|
||||||
|
@JsonProperty("postal_code")
|
||||||
|
protected String postalCode;
|
||||||
|
|
||||||
|
@JsonProperty("country")
|
||||||
|
protected String country;
|
||||||
|
|
||||||
|
@JsonProperty("claims_locales")
|
||||||
|
protected String claimsLocales;
|
||||||
|
|
||||||
|
public String getSubject() {
|
||||||
|
return this.sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubject(String subject) {
|
||||||
|
this.sub = subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGivenName() {
|
||||||
|
return this.givenName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGivenName(String givenName) {
|
||||||
|
this.givenName = givenName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFamilyName() {
|
||||||
|
return this.familyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFamilyName(String familyName) {
|
||||||
|
this.familyName = familyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMiddleName() {
|
||||||
|
return this.middleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMiddleName(String middleName) {
|
||||||
|
this.middleName = middleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNickName() {
|
||||||
|
return this.nickName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNickName(String nickName) {
|
||||||
|
this.nickName = nickName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPreferredUsername() {
|
||||||
|
return this.preferredUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPreferredUsername(String preferredUsername) {
|
||||||
|
this.preferredUsername = preferredUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProfile() {
|
||||||
|
return this.profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProfile(String profile) {
|
||||||
|
this.profile = profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPicture() {
|
||||||
|
return this.picture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPicture(String picture) {
|
||||||
|
this.picture = picture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWebsite() {
|
||||||
|
return this.website;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWebsite(String website) {
|
||||||
|
this.website = website;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return this.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmail(String email) {
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getEmailVerified() {
|
||||||
|
return this.emailVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmailVerified(Boolean emailVerified) {
|
||||||
|
this.emailVerified = emailVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGender() {
|
||||||
|
return this.gender;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGender(String gender) {
|
||||||
|
this.gender = gender;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBirthdate() {
|
||||||
|
return this.birthdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBirthdate(String birthdate) {
|
||||||
|
this.birthdate = birthdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getZoneinfo() {
|
||||||
|
return this.zoneinfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setZoneinfo(String zoneinfo) {
|
||||||
|
this.zoneinfo = zoneinfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLocale() {
|
||||||
|
return this.locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLocale(String locale) {
|
||||||
|
this.locale = locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhoneNumber() {
|
||||||
|
return this.phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhoneNumber(String phoneNumber) {
|
||||||
|
this.phoneNumber = phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getPhoneNumberVerified() {
|
||||||
|
return this.phoneNumberVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhoneNumberVerified(Boolean phoneNumberVerified) {
|
||||||
|
this.phoneNumberVerified = phoneNumberVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
return this.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAddress(String address) {
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUpdatedAt() {
|
||||||
|
return this.updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(Long updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSub() {
|
||||||
|
return this.sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSub(String sub) {
|
||||||
|
this.sub = sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFormattedAddress() {
|
||||||
|
return this.formattedAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFormattedAddress(String formattedAddress) {
|
||||||
|
this.formattedAddress = formattedAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStreetAddress() {
|
||||||
|
return this.streetAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStreetAddress(String streetAddress) {
|
||||||
|
this.streetAddress = streetAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLocality() {
|
||||||
|
return this.locality;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLocality(String locality) {
|
||||||
|
this.locality = locality;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRegion() {
|
||||||
|
return this.region;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegion(String region) {
|
||||||
|
this.region = region;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPostalCode() {
|
||||||
|
return this.postalCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPostalCode(String postalCode) {
|
||||||
|
this.postalCode = postalCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCountry() {
|
||||||
|
return this.country;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCountry(String country) {
|
||||||
|
this.country = country;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClaimsLocales() {
|
||||||
|
return this.claimsLocales;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClaimsLocales(String claimsLocales) {
|
||||||
|
this.claimsLocales = claimsLocales;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* JBoss, Home of Professional Open Source
|
||||||
|
*
|
||||||
|
* Copyright 2013 Red Hat, Inc. and/or its affiliates.
|
||||||
|
*
|
||||||
|
* 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 org.keycloak.representations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author pedroigor
|
||||||
|
*/
|
||||||
|
public class UserInfo extends UserClaimSet {
|
||||||
|
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import org.keycloak.jose.jws.JWSBuilder;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.UserClaimSet;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
@ -58,7 +59,9 @@ public class SkeletonKeyTokenTest {
|
||||||
public void testSerialization() throws Exception {
|
public void testSerialization() throws Exception {
|
||||||
AccessToken token = createSimpleToken();
|
AccessToken token = createSimpleToken();
|
||||||
IDToken idToken = new IDToken();
|
IDToken idToken = new IDToken();
|
||||||
idToken.setEmail("joe@email.cz");
|
UserClaimSet claimSet = idToken.getUserClaimSet();
|
||||||
|
|
||||||
|
claimSet.setEmail("joe@email.cz");
|
||||||
|
|
||||||
KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
|
KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
|
||||||
|
|
||||||
|
@ -95,7 +98,7 @@ public class SkeletonKeyTokenTest {
|
||||||
Assert.assertEquals("111", token.getId());
|
Assert.assertEquals("111", token.getId());
|
||||||
Assert.assertTrue(token.getResourceAccess("foo").isUserInRole("admin"));
|
Assert.assertTrue(token.getResourceAccess("foo").isUserInRole("admin"));
|
||||||
Assert.assertTrue(token.getResourceAccess("bar").isUserInRole("user"));
|
Assert.assertTrue(token.getResourceAccess("bar").isUserInRole("user"));
|
||||||
Assert.assertEquals("joe@email.cz", idToken.getEmail());
|
Assert.assertEquals("joe@email.cz", claimSet.getEmail());
|
||||||
Assert.assertEquals("acme", ctx.getRealm());
|
Assert.assertEquals("acme", ctx.getRealm());
|
||||||
ois.close();
|
ois.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,8 @@ public enum EventType {
|
||||||
|
|
||||||
INVALID_SIGNATURE_ERROR,
|
INVALID_SIGNATURE_ERROR,
|
||||||
REGISTER_NODE,
|
REGISTER_NODE,
|
||||||
UNREGISTER_NODE
|
UNREGISTER_NODE,
|
||||||
|
|
||||||
|
USER_INFO_REQUEST,
|
||||||
|
USER_INFO_REQUEST_ERROR
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
|
<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
|
||||||
<%@ page import="org.keycloak.example.CustomerDatabaseClient" %>
|
<%@ page import="org.keycloak.example.CustomerDatabaseClient" %>
|
||||||
<%@ page import="org.keycloak.representations.IDToken" %>
|
<%@ page import="org.keycloak.representations.IDToken" %>
|
||||||
|
<%@ page import="org.keycloak.representations.UserClaimSet" %>
|
||||||
<%@ page import="org.keycloak.util.KeycloakUriBuilder" %>
|
<%@ page import="org.keycloak.util.KeycloakUriBuilder" %>
|
||||||
<%@ page session="false" %>
|
<%@ page session="false" %>
|
||||||
<html>
|
<html>
|
||||||
|
@ -16,17 +17,18 @@
|
||||||
String acctUri = KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.ACCOUNT_SERVICE_PATH)
|
String acctUri = KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.ACCOUNT_SERVICE_PATH)
|
||||||
.queryParam("referrer", "customer-portal").build("demo").toString();
|
.queryParam("referrer", "customer-portal").build("demo").toString();
|
||||||
IDToken idToken = CustomerDatabaseClient.getIDToken(request);
|
IDToken idToken = CustomerDatabaseClient.getIDToken(request);
|
||||||
|
UserClaimSet claims = idToken.getUserClaimSet();
|
||||||
%>
|
%>
|
||||||
<p>Goto: <a href="/product-portal">products</a> | <a href="<%=logoutUri%>">logout</a> | <a
|
<p>Goto: <a href="/product-portal">products</a> | <a href="<%=logoutUri%>">logout</a> | <a
|
||||||
href="<%=acctUri%>">manage acct</a></p>
|
href="<%=acctUri%>">manage acct</a></p>
|
||||||
Servlet User Principal <b><%=request.getUserPrincipal().getName()%>
|
Servlet User Principal <b><%=request.getUserPrincipal().getName()%>
|
||||||
</b> made this request.
|
</b> made this request.
|
||||||
<p><b>Caller IDToken values</b> (<i>You can specify what is returned in IDToken in the customer-portal claims page in the admin console</i>:</p>
|
<p><b>Caller IDToken values</b> (<i>You can specify what is returned in IDToken in the customer-portal claims page in the admin console</i>:</p>
|
||||||
<p>Username: <%=idToken.getPreferredUsername()%></p>
|
<p>Username: <%=claims.getPreferredUsername()%></p>
|
||||||
<p>Email: <%=idToken.getEmail()%></p>
|
<p>Email: <%=claims.getEmail()%></p>
|
||||||
<p>Full Name: <%=idToken.getName()%></p>
|
<p>Full Name: <%=claims.getName()%></p>
|
||||||
<p>First: <%=idToken.getGivenName()%></p>
|
<p>First: <%=claims.getGivenName()%></p>
|
||||||
<p>Last: <%=idToken.getFamilyName()%></p>
|
<p>Last: <%=claims.getFamilyName()%></p>
|
||||||
<h2>Customer Listing</h2>
|
<h2>Customer Listing</h2>
|
||||||
<%
|
<%
|
||||||
java.util.List<String> list = null;
|
java.util.List<String> list = null;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<%@ page import="org.keycloak.representations.AccessTokenResponse" %>
|
<%@ page import="org.keycloak.representations.AccessTokenResponse" %>
|
||||||
<%@ page import="org.keycloak.representations.IDToken" %>
|
<%@ page import="org.keycloak.representations.IDToken" %>
|
||||||
<%@ page import="org.keycloak.servlet.ServletOAuthClient" %>
|
<%@ page import="org.keycloak.servlet.ServletOAuthClient" %>
|
||||||
|
<%@ page import="org.keycloak.representations.UserClaimSet" %>
|
||||||
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
|
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
|
||||||
pageEncoding="ISO-8859-1"%>
|
pageEncoding="ISO-8859-1"%>
|
||||||
<%@ page session="false" %>
|
<%@ page session="false" %>
|
||||||
|
@ -16,15 +17,16 @@
|
||||||
AccessTokenResponse tokenResponse = ProductDatabaseClient.getTokenResponse(request);
|
AccessTokenResponse tokenResponse = ProductDatabaseClient.getTokenResponse(request);
|
||||||
if (tokenResponse.getIdToken() != null) {
|
if (tokenResponse.getIdToken() != null) {
|
||||||
IDToken idToken = ServletOAuthClient.extractIdToken(tokenResponse.getIdToken());
|
IDToken idToken = ServletOAuthClient.extractIdToken(tokenResponse.getIdToken());
|
||||||
|
UserClaimSet claimSet = idToken.getUserClaimSet();
|
||||||
out.println("<p><i>Change client claims in admin console to view personal info of user</i></p>");
|
out.println("<p><i>Change client claims in admin console to view personal info of user</i></p>");
|
||||||
if (idToken.getPreferredUsername() != null) {
|
if (claimSet.getPreferredUsername() != null) {
|
||||||
out.println("<p>Username: " + idToken.getPreferredUsername() + "</p>");
|
out.println("<p>Username: " + claimSet.getPreferredUsername() + "</p>");
|
||||||
}
|
}
|
||||||
if (idToken.getName() != null) {
|
if (claimSet.getName() != null) {
|
||||||
out.println("<p>Full Name: " + idToken.getName() + "</p>");
|
out.println("<p>Full Name: " + claimSet.getName() + "</p>");
|
||||||
}
|
}
|
||||||
if (idToken.getEmail() != null) {
|
if (claimSet.getEmail() != null) {
|
||||||
out.println("<p>Email: " + idToken.getEmail() + "</p>");
|
out.println("<p>Email: " + claimSet.getEmail() + "</p>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
list = ProductDatabaseClient.getProducts(request, tokenResponse.getToken());
|
list = ProductDatabaseClient.getProducts(request, tokenResponse.getToken());
|
||||||
|
|
|
@ -16,14 +16,15 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.example.multitenant.boundary;
|
package org.keycloak.example.multitenant.boundary;
|
||||||
|
|
||||||
import java.io.IOException;
|
import org.keycloak.KeycloakPrincipal;
|
||||||
import java.io.PrintWriter;
|
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
import javax.servlet.annotation.WebServlet;
|
import javax.servlet.annotation.WebServlet;
|
||||||
import javax.servlet.http.HttpServlet;
|
import javax.servlet.http.HttpServlet;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import org.keycloak.KeycloakPrincipal;
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -54,7 +55,7 @@ public class ProtectedServlet extends HttpServlet {
|
||||||
writer.write(principal.getKeycloakSecurityContext().getIdToken().getIssuer());
|
writer.write(principal.getKeycloakSecurityContext().getIdToken().getIssuer());
|
||||||
|
|
||||||
writer.write("<br/>User: ");
|
writer.write("<br/>User: ");
|
||||||
writer.write(principal.getKeycloakSecurityContext().getIdToken().getPreferredUsername());
|
writer.write(principal.getKeycloakSecurityContext().getIdToken().getUserClaimSet().getPreferredUsername());
|
||||||
|
|
||||||
writer.write(String.format("<br/><a href=\"/multitenant/%s/logout\">Logout</a>", realm));
|
writer.write(String.format("<br/><a href=\"/multitenant/%s/logout\">Logout</a>", realm));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
package org.keycloak.adapters;
|
package org.keycloak.adapters;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.KeycloakPrincipal;
|
import org.keycloak.KeycloakPrincipal;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.UserClaimSet;
|
||||||
import org.keycloak.util.UriUtils;
|
import org.keycloak.util.UriUtils;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
|
@ -65,20 +66,22 @@ public class AdapterUtils {
|
||||||
String attr = "sub";
|
String attr = "sub";
|
||||||
if (deployment.getPrincipalAttribute() != null) attr = deployment.getPrincipalAttribute();
|
if (deployment.getPrincipalAttribute() != null) attr = deployment.getPrincipalAttribute();
|
||||||
String name = null;
|
String name = null;
|
||||||
|
UserClaimSet claimSet = token.getUserClaimSet();
|
||||||
|
|
||||||
if ("sub".equals(attr)) {
|
if ("sub".equals(attr)) {
|
||||||
name = token.getSubject();
|
name = token.getSubject();
|
||||||
} else if ("email".equals(attr)) {
|
} else if ("email".equals(attr)) {
|
||||||
name = token.getEmail();
|
name = claimSet.getEmail();
|
||||||
} else if ("preferred_username".equals(attr)) {
|
} else if ("preferred_username".equals(attr)) {
|
||||||
name = token.getPreferredUsername();
|
name = claimSet.getPreferredUsername();
|
||||||
} else if ("name".equals(attr)) {
|
} else if ("name".equals(attr)) {
|
||||||
name = token.getName();
|
name = claimSet.getName();
|
||||||
} else if ("given_name".equals(attr)) {
|
} else if ("given_name".equals(attr)) {
|
||||||
name = token.getGivenName();
|
name = claimSet.getGivenName();
|
||||||
} else if ("family_name".equals(attr)) {
|
} else if ("family_name".equals(attr)) {
|
||||||
name = token.getFamilyName();
|
name = claimSet.getFamilyName();
|
||||||
} else if ("nickname".equals(attr)) {
|
} else if ("nickname".equals(attr)) {
|
||||||
name = token.getNickName();
|
name = claimSet.getNickName();
|
||||||
}
|
}
|
||||||
if (name == null) name = token.getSubject();
|
if (name == null) name = token.getSubject();
|
||||||
return name;
|
return name;
|
||||||
|
|
|
@ -204,6 +204,31 @@
|
||||||
return promise.promise;
|
return promise.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kc.loadUserInfo = function() {
|
||||||
|
var url = getRealmUrl() + '/protocol/openid-connect/userinfo';
|
||||||
|
var req = new XMLHttpRequest();
|
||||||
|
req.open('GET', url, true);
|
||||||
|
req.setRequestHeader('Accept', 'application/json');
|
||||||
|
req.setRequestHeader('Authorization', 'bearer ' + kc.token);
|
||||||
|
|
||||||
|
var promise = createPromise();
|
||||||
|
|
||||||
|
req.onreadystatechange = function () {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
if (req.status == 200) {
|
||||||
|
kc.userInfo = JSON.parse(req.responseText);
|
||||||
|
promise.setSuccess(kc.userInfo);
|
||||||
|
} else {
|
||||||
|
promise.setError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.send();
|
||||||
|
|
||||||
|
return promise.promise;
|
||||||
|
}
|
||||||
|
|
||||||
kc.isTokenExpired = function(minValidity) {
|
kc.isTokenExpired = function(minValidity) {
|
||||||
if (!kc.tokenParsed || !kc.refreshToken) {
|
if (!kc.tokenParsed || !kc.refreshToken) {
|
||||||
throw 'Not authenticated';
|
throw 'Not authenticated';
|
||||||
|
|
|
@ -4,10 +4,9 @@ import io.undertow.server.HttpHandler;
|
||||||
import io.undertow.server.HttpServerExchange;
|
import io.undertow.server.HttpServerExchange;
|
||||||
import io.undertow.util.HttpString;
|
import io.undertow.util.HttpString;
|
||||||
import org.keycloak.adapters.undertow.KeycloakUndertowAccount;
|
import org.keycloak.adapters.undertow.KeycloakUndertowAccount;
|
||||||
|
import org.keycloak.representations.UserClaimSet;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
|
@ -65,14 +64,17 @@ public class ConstraintAuthorizationHandler implements HttpHandler {
|
||||||
if (idToken.getSubject() != null) {
|
if (idToken.getSubject() != null) {
|
||||||
exchange.getRequestHeaders().put(KEYCLOAK_SUBJECT, idToken.getSubject());
|
exchange.getRequestHeaders().put(KEYCLOAK_SUBJECT, idToken.getSubject());
|
||||||
}
|
}
|
||||||
if (idToken.getPreferredUsername() != null) {
|
|
||||||
exchange.getRequestHeaders().put(KEYCLOAK_USERNAME, idToken.getPreferredUsername());
|
UserClaimSet claimSet = idToken.getUserClaimSet();
|
||||||
|
|
||||||
|
if (claimSet.getPreferredUsername() != null) {
|
||||||
|
exchange.getRequestHeaders().put(KEYCLOAK_USERNAME, claimSet.getPreferredUsername());
|
||||||
}
|
}
|
||||||
if (idToken.getEmail() != null) {
|
if (claimSet.getEmail() != null) {
|
||||||
exchange.getRequestHeaders().put(KEYCLOAK_EMAIL, idToken.getEmail());
|
exchange.getRequestHeaders().put(KEYCLOAK_EMAIL, claimSet.getEmail());
|
||||||
}
|
}
|
||||||
if (idToken.getName() != null) {
|
if (claimSet.getName() != null) {
|
||||||
exchange.getRequestHeaders().put(KEYCLOAK_NAME, idToken.getName());
|
exchange.getRequestHeaders().put(KEYCLOAK_NAME, claimSet.getName());
|
||||||
}
|
}
|
||||||
if (sendAccessToken) {
|
if (sendAccessToken) {
|
||||||
exchange.getRequestHeaders().put(KEYCLOAK_ACCESS_TOKEN, account.getKeycloakSecurityContext().getTokenString());
|
exchange.getRequestHeaders().put(KEYCLOAK_ACCESS_TOKEN, account.getKeycloakSecurityContext().getTokenString());
|
||||||
|
|
|
@ -8,6 +8,7 @@ import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.jboss.resteasy.spi.HttpResponse;
|
import org.jboss.resteasy.spi.HttpResponse;
|
||||||
import org.jboss.resteasy.spi.NotAcceptableException;
|
import org.jboss.resteasy.spi.NotAcceptableException;
|
||||||
import org.jboss.resteasy.spi.NotFoundException;
|
import org.jboss.resteasy.spi.NotFoundException;
|
||||||
|
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||||
import org.jboss.resteasy.spi.UnauthorizedException;
|
import org.jboss.resteasy.spi.UnauthorizedException;
|
||||||
import org.keycloak.ClientConnection;
|
import org.keycloak.ClientConnection;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
|
@ -683,6 +684,15 @@ public class OpenIDConnectService {
|
||||||
return Cors.add(request, Response.ok(res)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
|
return Cors.add(request, Response.ok(res)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Path("userinfo")
|
||||||
|
public Object issueUserInfo() {
|
||||||
|
UserInfoService userInfoEndpoint = new UserInfoService(this);
|
||||||
|
|
||||||
|
ResteasyProviderFactory.getInstance().injectProperties(userInfoEndpoint);
|
||||||
|
|
||||||
|
return userInfoEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap<String, String> formData, EventBuilder event) {
|
protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap<String, String> formData, EventBuilder event) {
|
||||||
ClientModel client = authorizeClientBase(authorizationHeader, formData, event, realm);
|
ClientModel client = authorizeClientBase(authorizationHeader, formData, event, realm);
|
||||||
|
|
||||||
|
@ -1148,4 +1158,11 @@ public class OpenIDConnectService {
|
||||||
return Response.status(status).entity(e).type("application/json").build();
|
return Response.status(status).entity(e).type("application/json").build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TokenManager getTokenManager() {
|
||||||
|
return this.tokenManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
RealmModel getRealm() {
|
||||||
|
return this.realm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
|
import org.keycloak.representations.UserClaimSet;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.representations.RefreshToken;
|
import org.keycloak.representations.RefreshToken;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
@ -204,21 +205,23 @@ public class TokenManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initClaims(IDToken token, ClientModel model, UserModel user) {
|
public void initClaims(UserClaimSet claimSet, ClientModel model, UserModel user) {
|
||||||
|
claimSet.setSubject(user.getId());
|
||||||
|
|
||||||
if (ClaimMask.hasUsername(model.getAllowedClaimsMask())) {
|
if (ClaimMask.hasUsername(model.getAllowedClaimsMask())) {
|
||||||
token.setPreferredUsername(user.getUsername());
|
claimSet.setPreferredUsername(user.getUsername());
|
||||||
}
|
}
|
||||||
if (ClaimMask.hasEmail(model.getAllowedClaimsMask())) {
|
if (ClaimMask.hasEmail(model.getAllowedClaimsMask())) {
|
||||||
token.setEmail(user.getEmail());
|
claimSet.setEmail(user.getEmail());
|
||||||
token.setEmailVerified(user.isEmailVerified());
|
claimSet.setEmailVerified(user.isEmailVerified());
|
||||||
}
|
}
|
||||||
if (ClaimMask.hasName(model.getAllowedClaimsMask())) {
|
if (ClaimMask.hasName(model.getAllowedClaimsMask())) {
|
||||||
token.setFamilyName(user.getLastName());
|
claimSet.setFamilyName(user.getLastName());
|
||||||
token.setGivenName(user.getFirstName());
|
claimSet.setGivenName(user.getFirstName());
|
||||||
StringBuilder fullName = new StringBuilder();
|
StringBuilder fullName = new StringBuilder();
|
||||||
if (user.getFirstName() != null) fullName.append(user.getFirstName()).append(" ");
|
if (user.getFirstName() != null) fullName.append(user.getFirstName()).append(" ");
|
||||||
if (user.getLastName() != null) fullName.append(user.getLastName());
|
if (user.getLastName() != null) fullName.append(user.getLastName());
|
||||||
token.setName(fullName.toString());
|
claimSet.setName(fullName.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,7 +236,8 @@ public class TokenManager {
|
||||||
if (realm.getAccessTokenLifespan() > 0) {
|
if (realm.getAccessTokenLifespan() > 0) {
|
||||||
token.expiration(Time.currentTime() + realm.getAccessTokenLifespan());
|
token.expiration(Time.currentTime() + realm.getAccessTokenLifespan());
|
||||||
}
|
}
|
||||||
initClaims(token, claimer, user);
|
UserClaimSet claimSet = token.getUserClaimSet();
|
||||||
|
initClaims(claimSet, claimer, user);
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,7 +261,8 @@ public class TokenManager {
|
||||||
if (allowedOrigins != null) {
|
if (allowedOrigins != null) {
|
||||||
token.setAllowedOrigins(allowedOrigins);
|
token.setAllowedOrigins(allowedOrigins);
|
||||||
}
|
}
|
||||||
initClaims(token, client, user);
|
UserClaimSet claimSet = token.getUserClaimSet();
|
||||||
|
initClaims(claimSet, client, user);
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,30 +359,30 @@ public class TokenManager {
|
||||||
if (realm.getAccessTokenLifespan() > 0) {
|
if (realm.getAccessTokenLifespan() > 0) {
|
||||||
idToken.expiration(Time.currentTime() + realm.getAccessTokenLifespan());
|
idToken.expiration(Time.currentTime() + realm.getAccessTokenLifespan());
|
||||||
}
|
}
|
||||||
idToken.setPreferredUsername(accessToken.getPreferredUsername());
|
idToken.getUserClaimSet().setPreferredUsername(accessToken.getUserClaimSet().getPreferredUsername());
|
||||||
idToken.setGivenName(accessToken.getGivenName());
|
idToken.getUserClaimSet().setGivenName(accessToken.getUserClaimSet().getGivenName());
|
||||||
idToken.setMiddleName(accessToken.getMiddleName());
|
idToken.getUserClaimSet().setMiddleName(accessToken.getUserClaimSet().getMiddleName());
|
||||||
idToken.setFamilyName(accessToken.getFamilyName());
|
idToken.getUserClaimSet().setFamilyName(accessToken.getUserClaimSet().getFamilyName());
|
||||||
idToken.setName(accessToken.getName());
|
idToken.getUserClaimSet().setName(accessToken.getUserClaimSet().getName());
|
||||||
idToken.setNickName(accessToken.getNickName());
|
idToken.getUserClaimSet().setNickName(accessToken.getUserClaimSet().getNickName());
|
||||||
idToken.setGender(accessToken.getGender());
|
idToken.getUserClaimSet().setGender(accessToken.getUserClaimSet().getGender());
|
||||||
idToken.setPicture(accessToken.getPicture());
|
idToken.getUserClaimSet().setPicture(accessToken.getUserClaimSet().getPicture());
|
||||||
idToken.setProfile(accessToken.getProfile());
|
idToken.getUserClaimSet().setProfile(accessToken.getUserClaimSet().getProfile());
|
||||||
idToken.setWebsite(accessToken.getWebsite());
|
idToken.getUserClaimSet().setWebsite(accessToken.getUserClaimSet().getWebsite());
|
||||||
idToken.setBirthdate(accessToken.getBirthdate());
|
idToken.getUserClaimSet().setBirthdate(accessToken.getUserClaimSet().getBirthdate());
|
||||||
idToken.setEmail(accessToken.getEmail());
|
idToken.getUserClaimSet().setEmail(accessToken.getUserClaimSet().getEmail());
|
||||||
idToken.setEmailVerified(accessToken.getEmailVerified());
|
idToken.getUserClaimSet().setEmailVerified(accessToken.getUserClaimSet().getEmailVerified());
|
||||||
idToken.setLocale(accessToken.getLocale());
|
idToken.getUserClaimSet().setLocale(accessToken.getUserClaimSet().getLocale());
|
||||||
idToken.setFormattedAddress(accessToken.getFormattedAddress());
|
idToken.getUserClaimSet().setFormattedAddress(accessToken.getUserClaimSet().getFormattedAddress());
|
||||||
idToken.setAddress(accessToken.getAddress());
|
idToken.getUserClaimSet().setAddress(accessToken.getUserClaimSet().getAddress());
|
||||||
idToken.setStreetAddress(accessToken.getStreetAddress());
|
idToken.getUserClaimSet().setStreetAddress(accessToken.getUserClaimSet().getStreetAddress());
|
||||||
idToken.setLocality(accessToken.getLocality());
|
idToken.getUserClaimSet().setLocality(accessToken.getUserClaimSet().getLocality());
|
||||||
idToken.setRegion(accessToken.getRegion());
|
idToken.getUserClaimSet().setRegion(accessToken.getUserClaimSet().getRegion());
|
||||||
idToken.setPostalCode(accessToken.getPostalCode());
|
idToken.getUserClaimSet().setPostalCode(accessToken.getUserClaimSet().getPostalCode());
|
||||||
idToken.setCountry(accessToken.getCountry());
|
idToken.getUserClaimSet().setCountry(accessToken.getUserClaimSet().getCountry());
|
||||||
idToken.setPhoneNumber(accessToken.getPhoneNumber());
|
idToken.getUserClaimSet().setPhoneNumber(accessToken.getUserClaimSet().getPhoneNumber());
|
||||||
idToken.setPhoneNumberVerified(accessToken.getPhoneNumberVerified());
|
idToken.getUserClaimSet().setPhoneNumberVerified(accessToken.getUserClaimSet().getPhoneNumberVerified());
|
||||||
idToken.setZoneinfo(accessToken.getZoneinfo());
|
idToken.getUserClaimSet().setZoneinfo(accessToken.getUserClaimSet().getZoneinfo());
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
* JBoss, Home of Professional Open Source
|
||||||
|
*
|
||||||
|
* Copyright 2013 Red Hat, Inc. and/or its affiliates.
|
||||||
|
*
|
||||||
|
* 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 org.keycloak.protocol.oidc;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
import org.jboss.resteasy.spi.HttpResponse;
|
||||||
|
import org.jboss.resteasy.spi.UnauthorizedException;
|
||||||
|
import org.keycloak.ClientConnection;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.UserClaimSet;
|
||||||
|
import org.keycloak.services.managers.AppAuthManager;
|
||||||
|
import org.keycloak.services.managers.EventsManager;
|
||||||
|
import org.keycloak.services.resources.Cors;
|
||||||
|
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.FormParam;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.OPTIONS;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author pedroigor
|
||||||
|
*/
|
||||||
|
public class UserInfoService {
|
||||||
|
|
||||||
|
@Context
|
||||||
|
private HttpRequest request;
|
||||||
|
|
||||||
|
@Context
|
||||||
|
private HttpResponse response;
|
||||||
|
|
||||||
|
@Context
|
||||||
|
private KeycloakSession session;
|
||||||
|
|
||||||
|
@Context
|
||||||
|
private ClientConnection clientConnection;
|
||||||
|
|
||||||
|
private final TokenManager tokenManager;
|
||||||
|
private final AppAuthManager appAuthManager;
|
||||||
|
private final OpenIDConnectService openIdConnectService;
|
||||||
|
private final RealmModel realmModel;
|
||||||
|
|
||||||
|
public UserInfoService(OpenIDConnectService openIDConnectService) {
|
||||||
|
this.realmModel = openIDConnectService.getRealm();
|
||||||
|
|
||||||
|
if (this.realmModel == null) {
|
||||||
|
throw new RuntimeException("Null realm.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tokenManager = openIDConnectService.getTokenManager();
|
||||||
|
|
||||||
|
if (this.tokenManager == null) {
|
||||||
|
throw new RuntimeException("Null token manager.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openIdConnectService = openIDConnectService;
|
||||||
|
this.appAuthManager = new AppAuthManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("/")
|
||||||
|
@OPTIONS
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Response issueUserInfoPreflight() {
|
||||||
|
return Cors.add(this.request, Response.ok()).auth().preflight().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("/")
|
||||||
|
@GET
|
||||||
|
@NoCache
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Response issueUserInfoGet(@Context final HttpHeaders headers) {
|
||||||
|
String accessToken = this.appAuthManager.extractAuthorizationHeaderToken(headers);
|
||||||
|
return issueUserInfo(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("/")
|
||||||
|
@POST
|
||||||
|
@NoCache
|
||||||
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Response issueUserInfoPost(@FormParam("access_token") String accessToken) {
|
||||||
|
return issueUserInfo(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response issueUserInfo(String token) {
|
||||||
|
try {
|
||||||
|
EventBuilder event = new EventsManager(this.realmModel, this.session, this.clientConnection).createEventBuilder()
|
||||||
|
.event(EventType.USER_INFO_REQUEST)
|
||||||
|
.detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN);
|
||||||
|
|
||||||
|
Response validationResponse = this.openIdConnectService.validateAccessToken(token);
|
||||||
|
|
||||||
|
if (!AccessToken.class.isInstance(validationResponse.getEntity())) {
|
||||||
|
event.error(EventType.USER_INFO_REQUEST.name());
|
||||||
|
return Response.fromResponse(validationResponse).status(Status.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessToken accessToken = (AccessToken) validationResponse.getEntity();
|
||||||
|
UserSessionModel userSession = session.sessions().getUserSession(realmModel, accessToken.getSessionState());
|
||||||
|
ClientModel clientModel = realmModel.findClient(accessToken.getIssuedFor());
|
||||||
|
UserModel userModel = userSession.getUser();
|
||||||
|
UserClaimSet userInfo = new UserClaimSet();
|
||||||
|
|
||||||
|
this.tokenManager.initClaims(userInfo, clientModel, userModel);
|
||||||
|
|
||||||
|
event
|
||||||
|
.detail(Details.USERNAME, userModel.getUsername())
|
||||||
|
.client(clientModel)
|
||||||
|
.session(userSession)
|
||||||
|
.user(userModel)
|
||||||
|
.success();
|
||||||
|
|
||||||
|
return Cors.add(request, Response.ok(userInfo)).auth().allowedOrigins(accessToken).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new UnauthorizedException("Could not retrieve user info.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,13 +16,14 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.adapter;
|
package org.keycloak.testsuite.adapter;
|
||||||
|
|
||||||
import java.io.IOException;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
import java.io.PrintWriter;
|
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
import javax.servlet.http.HttpServlet;
|
import javax.servlet.http.HttpServlet;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -37,7 +38,7 @@ public class MultiTenantServlet extends HttpServlet {
|
||||||
KeycloakSecurityContext context = (KeycloakSecurityContext)req.getAttribute(KeycloakSecurityContext.class.getName());
|
KeycloakSecurityContext context = (KeycloakSecurityContext)req.getAttribute(KeycloakSecurityContext.class.getName());
|
||||||
|
|
||||||
pw.print("Username: ");
|
pw.print("Username: ");
|
||||||
pw.println(context.getIdToken().getPreferredUsername());
|
pw.println(context.getIdToken().getUserClaimSet().getPreferredUsername());
|
||||||
|
|
||||||
pw.print("<br/>Realm: ");
|
pw.print("<br/>Realm: ");
|
||||||
pw.println(context.getRealm());
|
pw.println(context.getRealm());
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
* JBoss, Home of Professional Open Source.
|
||||||
|
* Copyright 2012, Red Hat, Inc., and individual contributors
|
||||||
|
* as indicated by the @author tags. See the copyright.txt file in the
|
||||||
|
* distribution for a full listing of individual contributors.
|
||||||
|
*
|
||||||
|
* This is free software; you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Lesser General Public License as
|
||||||
|
* published by the Free Software Foundation; either version 2.1 of
|
||||||
|
* the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This software is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this software; if not, write to the Free
|
||||||
|
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
||||||
|
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
|
||||||
|
*/
|
||||||
|
package org.keycloak.testsuite.oidc;
|
||||||
|
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.protocol.oidc.OpenIDConnectService;
|
||||||
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
|
import org.keycloak.representations.UserInfo;
|
||||||
|
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||||
|
import org.keycloak.testsuite.rule.WebResource;
|
||||||
|
import org.keycloak.testsuite.rule.WebRule;
|
||||||
|
import org.keycloak.util.BasicAuthHelper;
|
||||||
|
import org.openqa.selenium.WebDriver;
|
||||||
|
|
||||||
|
import javax.ws.rs.client.Client;
|
||||||
|
import javax.ws.rs.client.ClientBuilder;
|
||||||
|
import javax.ws.rs.client.Entity;
|
||||||
|
import javax.ws.rs.client.WebTarget;
|
||||||
|
import javax.ws.rs.core.Form;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author pedroigor
|
||||||
|
*/
|
||||||
|
public class UserInfoTest {
|
||||||
|
|
||||||
|
private static RealmModel realm;
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static KeycloakRule keycloakRule = new KeycloakRule();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public WebRule webRule = new WebRule(this);
|
||||||
|
|
||||||
|
@WebResource
|
||||||
|
protected WebDriver driver;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccessfulUserInfoRequest() throws Exception {
|
||||||
|
Client client = ClientBuilder.newClient();
|
||||||
|
UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
|
||||||
|
URI grantUri = OpenIDConnectService.grantAccessTokenUrl(builder).build("test");
|
||||||
|
WebTarget grantTarget = client.target(grantUri);
|
||||||
|
AccessTokenResponse accessTokenResponse = executeGrantAccessTokenRequest(grantTarget);
|
||||||
|
Response response = executeUserInfoRequest(accessTokenResponse.getToken());
|
||||||
|
|
||||||
|
assertEquals(Status.OK.getStatusCode(), response.getStatus());
|
||||||
|
|
||||||
|
UserInfo userInfo = response.readEntity(UserInfo.class);
|
||||||
|
|
||||||
|
response.close();
|
||||||
|
|
||||||
|
assertNotNull(userInfo);
|
||||||
|
assertNotNull(userInfo.getSubject());
|
||||||
|
assertEquals("test-user@localhost", userInfo.getEmail());
|
||||||
|
assertEquals("test-user@localhost", userInfo.getPreferredUsername());
|
||||||
|
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUnsuccessfulUserInfoRequest() throws Exception {
|
||||||
|
Response response = executeUserInfoRequest("bad");
|
||||||
|
|
||||||
|
response.close();
|
||||||
|
|
||||||
|
assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AccessTokenResponse executeGrantAccessTokenRequest(WebTarget grantTarget) {
|
||||||
|
String header = BasicAuthHelper.createHeader("test-app", "password");
|
||||||
|
Form form = new Form();
|
||||||
|
form.param("username", "test-user@localhost")
|
||||||
|
.param("password", "password");
|
||||||
|
|
||||||
|
Response response = grantTarget.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, header)
|
||||||
|
.post(Entity.form(form));
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
|
||||||
|
AccessTokenResponse accessTokenResponse = response.readEntity(AccessTokenResponse.class);
|
||||||
|
|
||||||
|
response.close();
|
||||||
|
|
||||||
|
return accessTokenResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response executeUserInfoRequest(String accessToken) {
|
||||||
|
UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
|
||||||
|
UriBuilder uriBuilder = OpenIDConnectService.tokenServiceBaseUrl(builder);
|
||||||
|
URI userInfoUri = uriBuilder.path(OpenIDConnectService.class, "issueUserInfo").build("test");
|
||||||
|
Client client = ClientBuilder.newClient();
|
||||||
|
WebTarget userInfoTarget = client.target(userInfoUri);
|
||||||
|
|
||||||
|
return userInfoTarget.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, "bearer " + accessToken)
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue