We are trying to implement SMART On FHIR healthcare authorization protocol specification. This spec is an extension to OIDC (open id connect protocol). In SMART on FHIR, we need to add extra claims called 'patient' with value say '123' in AccessTokenResponse object during the OAUTH dance.
In order to accomplish this, I tried to extended the OIDCLoginProtocol and OIDCLoginProtocolFactory classes and given a new name to this protocol called 'smart-openid-connect'. I created this as a SPI (service provider interface) JAR and copied it to /standalone/deployments folder. Now, I can see the new protocol called 'smart-openid-connect' in the UI, but it does not show Access Type options in the client creation screen to select as a confidential client. Hence, I am not able to create client secrets as the Credentials menu is not appearing for this new protocol.
I have the following questions:
How to enable the Credentials tab in the client creation screen using SPI for the new protocol that I created.?
Which class I need to override to add extra claims in AccessTokenResponse ?
Kindly help me in this regard.
Thanks for your help in advance.
I have followed your steps for developing our custom protocol. When we migrate our company existed authentication protocol, I have used org.keycloak.adapters.authentication.ClientCredentialsProvider, org.keycloak.authentication.ClientAuthenticatorFactory, org.keycloak.authentication.ClientAuthenticator classes for defining our custom protocol. Credentials tab is only visible if oidc and confidential choices are selected. It is defined on UI javascript codes. So we choose oidc option for setting custom protocol. Afterwards, we return back to our custom protocol.
XyzClientAuthenticatorFactory
public class XyzClientAuthenticatorFactory implements ClientAuthenticatorFactory, ClientAuthenticator {
public static final String PROVIDER_ID = "xyz-client-authenticator";
public static final String DISPLAY_TEXT = "Xyz Client Authenticator";
public static final String REFERENCE_CATEGORY = null;
public static final String HELP_TEXT = null;
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
private AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
AuthenticationExecutionModel.Requirement.DISABLED};
static {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
property.setName(Constants.CLIENT_SETTINGS_APP_ID);
property.setLabel("Xyz App Id");
property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(Constants.CLIENT_SETTINGS_APP_KEY);
property.setLabel("Xyz App Key");
property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property);
}
#Override
public void authenticateClient(ClientAuthenticationFlowContext context) {
}
#Override
public String getDisplayType() {
return DISPLAY_TEXT;
}
#Override
public String getReferenceCategory() {
return REFERENCE_CATEGORY;
}
#Override
public ClientAuthenticator create() {
return this;
}
#Override
public boolean isConfigurable() {
return false;
}
#Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
#Override
public boolean isUserSetupAllowed() {
return false;
}
#Override
public List<ProviderConfigProperty> getConfigPropertiesPerClient() {
return configProperties;
}
#Override
public Map<String, Object> getAdapterConfiguration(ClientModel client) {
Map<String, Object> result = new HashMap<>();
result.put(Constants.CLIENT_SETTINGS_APP_ID, client.getAttribute(Constants.CLIENT_SETTINGS_APP_ID));
result.put(Constants.CLIENT_SETTINGS_APP_KEY, client.getAttribute(Constants.CLIENT_SETTINGS_APP_KEY));
return result;
}
#Override
public Set<String> getProtocolAuthenticatorMethods(String loginProtocol) {
if (loginProtocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
Set<String> results = new LinkedHashSet<>();
results.add(Constants.CLIENT_SETTINGS_APP_ID);
results.add(Constants.CLIENT_SETTINGS_APP_KEY);
return results;
} else {
return Collections.emptySet();
}
}
#Override
public String getHelpText() {
return HELP_TEXT;
}
#Override
public List<ProviderConfigProperty> getConfigProperties() {
return new LinkedList<>();
}
#Override
public ClientAuthenticator create(KeycloakSession session) {
return this;
}
#Override
public void init(Config.Scope config) {
}
#Override
public void postInit(KeycloakSessionFactory factory) {
}
#Override
public void close() {
}
#Override
public String getId() {
return PROVIDER_ID;
}
}
XyzClientCredential
public class XyzClientCredential implements ClientCredentialsProvider {
public static final String PROVIDER_ID = "xyz-client-credential";
#Override
public String getId() {
return PROVIDER_ID;
}
#Override
public void init(KeycloakDeployment deployment, Object config) {
}
#Override
public void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
}
}
XyzLoginProtocolFactory
public class XyzLoginProtocolFactory implements LoginProtocolFactory {
static {
}
#Override
public Map<String, ProtocolMapperModel> getBuiltinMappers() {
return new HashMap<>();
}
#Override
public Object createProtocolEndpoint(RealmModel realm, EventBuilder event) {
return new XyzLoginProtocolService(realm, event);
}
protected void addDefaultClientScopes(RealmModel realm, ClientModel newClient) {
addDefaultClientScopes(realm, Arrays.asList(newClient));
}
protected void addDefaultClientScopes(RealmModel realm, List<ClientModel> newClients) {
Set<ClientScopeModel> defaultClientScopes = realm.getDefaultClientScopes(true).stream()
.filter(clientScope -> getId().equals(clientScope.getProtocol()))
.collect(Collectors.toSet());
for (ClientModel newClient : newClients) {
for (ClientScopeModel defaultClientScopeModel : defaultClientScopes) {
newClient.addClientScope(defaultClientScopeModel, true);
}
}
Set<ClientScopeModel> nonDefaultClientScopes = realm.getDefaultClientScopes(false).stream()
.filter(clientScope -> getId().equals(clientScope.getProtocol()))
.collect(Collectors.toSet());
for (ClientModel newClient : newClients) {
for (ClientScopeModel nonDefaultClientScope : nonDefaultClientScopes) {
newClient.addClientScope(nonDefaultClientScope, true);
}
}
}
#Override
public void createDefaultClientScopes(RealmModel newRealm, boolean addScopesToExistingClients) {
// Create default client scopes for realm built-in clients too
if (addScopesToExistingClients) {
addDefaultClientScopes(newRealm, newRealm.getClients());
}
}
#Override
public void setupClientDefaults(ClientRepresentation rep, ClientModel newClient) {
}
#Override
public LoginProtocol create(KeycloakSession session) {
return new XyzLoginProtocol().setSession(session);
}
#Override
public void init(Config.Scope config) {
log.infof("XyzLoginProtocolFactory init");
}
#Override
public void postInit(KeycloakSessionFactory factory) {
factory.register(event -> {
if (event instanceof RealmModel.ClientCreationEvent) {
ClientModel client = ((RealmModel.ClientCreationEvent)event).getCreatedClient();
addDefaultClientScopes(client.getRealm(), client);
addDefaults(client);
}
});
}
protected void addDefaults(ClientModel client) {
}
#Override
public void close() {
}
#Override
public String getId() {
return XyzLoginProtocol.LOGIN_PROTOCOL;
}
}
XyzLoginProtocol
public class XyzLoginProtocol implements LoginProtocol {
public static final String LOGIN_PROTOCOL = "xyz";
protected KeycloakSession session;
protected RealmModel realm;
protected UriInfo uriInfo;
protected HttpHeaders headers;
protected EventBuilder event;
public XyzLoginProtocol(KeycloakSession session, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, EventBuilder event) {
this.session = session;
this.realm = realm;
this.uriInfo = uriInfo;
this.headers = headers;
this.event = event;
}
public XyzLoginProtocol() {
}
#Override
public XyzLoginProtocol setSession(KeycloakSession session) {
this.session = session;
return this;
}
#Override
public XyzLoginProtocol setRealm(RealmModel realm) {
this.realm = realm;
return this;
}
#Override
public XyzLoginProtocol setUriInfo(UriInfo uriInfo) {
this.uriInfo = uriInfo;
return this;
}
#Override
public XyzLoginProtocol setHttpHeaders(HttpHeaders headers) {
this.headers = headers;
return this;
}
#Override
public XyzLoginProtocol setEventBuilder(EventBuilder event) {
this.event = event;
return this;
}
#Override
public Response authenticated(AuthenticationSessionModel authSession, UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
log.debugf("Authenticated.. User: %s, Session Id: %s", userSession.getUser().getUsername(), userSession.getId());
try {
....
} catch (Exception ex) {
// TODO handle TokenNotFoundException exception
log.error(ex.getMessage(), ex);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
#Override
public Response sendError(AuthenticationSessionModel authSession, Error error) {
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true);
String redirect = authSession.getRedirectUri();
try {
URI uri = new URI(redirect);
return Response.status(302).location(uri).build();
} catch (Exception ex) {
log.error(ex.getMessage(), ex);
return Response.noContent().build();
}
}
#Override
public void backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
new ResourceAdminManager(session).logoutClientSession(realm, client, clientSession);
}
#Override
public Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
throw new RuntimeException("NOT IMPLEMENTED");
}
#Override
public Response finishLogout(UserSessionModel userSession) {
return Response.noContent().build();
}
#Override
public boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession) {
return false;
}
#Override
public boolean sendPushRevocationPolicyRequest(RealmModel realm, ClientModel resource, int notBefore, String managementUrl) {
PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getClientId(), notBefore);
String token = session.tokens().encode(adminAction);
log.tracev("pushRevocation resource: {0} url: {1}", resource.getClientId(), managementUrl);
URI target = UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_PUSH_NOT_BEFORE).build();
try {
int status = session.getProvider(HttpClientProvider.class).postText(target.toString(), token);
boolean success = status == 204 || status == 200;
log.tracef("pushRevocation success for %s: %s", managementUrl, success);
return success;
} catch (IOException e) {
ServicesLogger.LOGGER.failedToSendRevocation(e);
return false;
}
}
#Override
public void close() {
}
}
XyzLoginProtocolService
public class XyzLoginProtocolService {
private final RealmModel realm;
private final EventBuilder event;
#Context
private KeycloakSession session;
#Context
private HttpHeaders headers;
#Context
private HttpRequest request;
#Context
private ClientConnection clientConnection;
public XyzLoginProtocolService(RealmModel realm, EventBuilder event) {
this.realm = realm;
this.event = event;
this.event.realm(realm);
}
#POST
#Path("request")
#Produces(MediaType.APPLICATION_JSON)
#NoCache
public Response request(ApipmLoginRequest loginRequest) {
....
}
Below is the code I tried.
#Bean
public MultiResourceItemReader<Map<String, String>> multiResourceItemReader() throws FileNotFoundException {
MultiResourceItemReader<Map<String, String>> resourceItemReader = new MultiResourceItemReader<Map<String, String>>();
inputResources=getMultipleResourceItemreader();
resourceItemReader.setResources(inputResources);
resourceItemReader.setDelegate(reader());
return resourceItemReader;
}
You can use ResourceAware interface to get the resource name.Your Iteam should implement ResourceAware interface.
class Foo implements ResourceAware {
String value;
Resource resource;
Foo(String value) {
this.value = value;
}
#Override
public void setResource(Resource resource) {
this.resource = resource;
}
}
}
I have InfraNameModel (Rest-type) to work with JSON
public interface IInfraNameBeanFactory extends AutoBeanFactory {
IInfraNameBeanFactory INSTANCE = GWT.create(IInfraNameBeanFactory.class);
AutoBean<InfraNameModel> infraName();
AutoBean<InfraNameListModel> results();
}
public interface InfraNameListModel {
List<InfraNameModel> getResults();
void setResults(List<InfraNameModel> results);
}
public class InfraNameListModelImpl implements InfraNameListModel {
private List<InfraNameModel> results;
#Override
public List<InfraNameModel> getResults() {
return results;
}
#Override
public void setResults(List<InfraNameModel> results) {
this.results = results;
}
}
public interface InfraNameModel {
String getInfraName();
void setInfraName(String infraName);
}
public class InfraNameModelImpl implements InfraNameModel {
private String infraName;
#Override
public String getInfraName() {
return infraName;
}
#Override
public void setInfraName(String infraName) {
this.infraName = infraName;
}
}
I wanted to make them into a separate JAR
To make it common for the client and the server
But now I have errors
[WARN] Class by.models.infraNameModel.InfraNameModel is used in Gin, but not available in GWT client code.
Is it real to pull such beans into a separate library?
I try to save a LocalTime (joda) field to the MongoDB with SpringData using spring-boot-starter-parent (org.springframework.boot 1.2.3.RELEASE) and get a StackOverflowError.
The StackOverflowError is in BeanWrapper in the method
public <S> S getProperty(PersistentProperty<?> property, Class<? extends S> type)
Stacktrace:
http-nio-8080-exec-2#5509 daemon, prio=5, in group 'main', status: 'RUNNING'
at org.springframework.data.mapping.model.BeanWrapper.getProperty(BeanWrapper.java:120)
at org.springframework.data.mapping.model.BeanWrapper.getProperty(BeanWrapper.java:100)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$3.doWithPersistentProperty(MappingMongoConverter.java:419)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$3.doWithPersistentProperty(MappingMongoConverter.java:412)
at org.springframework.data.mapping.model.BasicPersistentEntity.doWithProperties(BasicPersistentEntity.java:307)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writeInternal(MappingMongoConverter.java:412)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writePropertyInternal(MappingMongoConverter.java:511)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$3.doWithPersistentProperty(MappingMongoConverter.java:424)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$3.doWithPersistentProperty(MappingMongoConverter.java:412)
at org.springframework.data.mapping.model.BasicPersistentEntity.doWithProperties(BasicPersistentEntity.java:307)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writeInternal(MappingMongoConverter.java:412)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writePropertyInternal(MappingMongoConverter.java:511)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$3.doWithPersistentProperty(MappingMongoConverter.java:424)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$3.doWithPersistentProperty(MappingMongoConverter.java:412)
at org.springframework.data.mapping.model.BasicPersistentEntity.doWithProperties(BasicPersistentEntity.java:307)...
Adding these two Converters to the CustomConversions fix the problem.
#Configuration
public class MongoConfiguration extends AbstractMongoConfiguration {
#Override
protected String getDatabaseName() {
return "databasename";
}
#Override
public Mongo mongo() throws Exception {
return new MongoClient("localhost");
}
#Override
public CustomConversions customConversions() {
List<Converter<?, ?>> converters = new ArrayList<>();
converters.add(new LocalTimeToStringConverter());
converters.add(new StringToLocalTimeConverter());
return new CustomConversions(converters);
}
}
public class LocalTimeToStringConverter implements Converter<LocalTime, String> {
#Override
public String convert(LocalTime localTime) {
return localTime.toString();
}
}
public class StringToLocalTimeConverter implements Converter<String, LocalTime> {
#Override
public LocalTime convert(String s) {
return LocalTime.parse(s);
}
}
GWT 2.5.0
A simple case using ListEditor failed below, what did i miss?
public class OneBean {
private String name;
public OneBean() {
}
public OneBean(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Override
public String toString() {
return "OneBean [name=" + name + "]";
}
}
public class OneListEditor extends Composite implements
IsEditor<ListEditor<OneBean, OneEditor>> {
interface OneListUiBinder extends UiBinder<Widget, OneListEditor> {}
OneListUiBinder uiBinder = GWT.create(OneListUiBinder.class);
#UiField
VerticalPanel panel;
public OneListEditor() {
initWidget(uiBinder.createAndBindUi(this));
}
#Override
public ListEditor<OneBean, OneEditor> asEditor() {
return listEditor;
}
private ListEditor<OneBean, OneEditor> listEditor = ListEditor
.of(new EditorSource<OneEditor>() {
#Override
public OneEditor create(int index) {
OneEditor widget = new OneEditor();
panel.insert(widget, index);
return widget;
}
});
}
public class OneEditor extends Composite implements Editor<OneBean> {
interface OneUiBinder extends UiBinder<Widget, OneEditor> {}
OneUiBinder uiBinder = GWT.create(OneUiBinder.class);
#UiField
TextBox name;
public OneEditor() {
initWidget(uiBinder.createAndBindUi(this));
}
}
public class OneListEditorApp implements EntryPoint {
#Override
public void onModuleLoad() {
List<OneBean> beans = new ArrayList<OneBean>();
beans.add(new OneBean("1st bean"));
beans.add(new OneBean("2nd bean"));
OneListEditor oneListEditor = new OneListEditor();
oneListEditor.asEditor().setValue(beans); // exception thrown here!
RootPanel.get().add(oneListEditor);
}
}
java.lang.NullPointerException: null
at com.google.gwt.editor.client.adapters.ListEditorWrapper.attach(ListEditorWrapper.java:95)
at com.google.gwt.editor.client.adapters.ListEditor.setValue(ListEditor.java:164)
at OneListEditorApp.onModuleLoad ....
void attach() {
editors.addAll(editorSource.create(workingCopy.size(), 0));
for (int i = 0, j = workingCopy.size(); i < j; i++) {
chain.attach(workingCopy.get(i), editors.get(i)); // ListEditorWrapper NPE here!
}
}
#EDIT
According to the answer from #Thomas Broyer, NPE is gone after EditDriver being wired to OneListEditor below,
interface OneEditorDriver extends
SimpleBeanEditorDriver<OneBean, OneEditor> {}
OneEditorDriver driver = GWT.create(OneEditorDriver.class);
#Override
public ListEditor<OneBean, OneEditor> asEditor() {
listEditor.setEditorChain(new EditorChain<OneBean, OneEditor>() {
#Override
public OneBean getValue(OneEditor subEditor) {
return null;
}
#Override
public void detach(OneEditor subEditor) {
}
#Override
public void attach(OneBean object, OneEditor subEditor) {
driver.initialize(subEditor);
driver.edit(object);
}
});
return listEditor;
}
You're not using an EditorDriver, so the ListEditor is not initialized with an EditorChain, so chain is null, hence the NPE. Case made.
⇒ use an EditorDriver (or do not use a ListEditor)