/*
* JBoss, Home of Professional Open Source.
* Copyright 2016 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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.wildfly.security.mechanism.oauth2;
import static org.wildfly.security._private.ElytronMessages.log;
import java.util.Map;
import java.util.NoSuchElementException;
import javax.json.Json;
import javax.json.JsonObjectBuilder;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.AuthorizeCallback;
import org.wildfly.security._private.ElytronMessages;
import org.wildfly.security.auth.callback.EvidenceVerifyCallback;
import org.wildfly.security.evidence.BearerTokenEvidence;
import org.wildfly.security.mechanism.AuthenticationMechanismException;
import org.wildfly.security.mechanism.MechanismUtil;
import org.wildfly.security.util.ByteIterator;
/**
* An OAuth2 Sasl Server based on RFC-7628.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class OAuth2Server {
public static final String CONFIG_OPENID_CONFIGURATION_URL = "openid-configuration";
private static final String KV_DELIMITER = "%x01";
private final String mechanismName;
private final CallbackHandler callbackHandler;
private final Map<String, ?> serverConfig;
public OAuth2Server(String mechanismName, CallbackHandler callbackHandler, Map<String, ?> serverConfig) {
this.mechanismName = mechanismName;
this.callbackHandler = callbackHandler;
this.serverConfig = serverConfig;
}
public OAuth2InitialClientMessage parseInitialClientMessage(byte[] fromBytes) throws AuthenticationMechanismException {
byte[] messageBytes = fromBytes.clone();
ByteIterator byteIterator = ByteIterator.ofBytes(fromBytes.clone());
try {
final char cbindFlag = (char) byteIterator.next();
if (cbindFlag != 'n') {
throw ElytronMessages.log.mechChannelBindingNotSupported(this.mechanismName);
}
String authorizationID = null;
if (byteIterator.next() == ',') {
final int c = byteIterator.next();
if (c == 'a') {
if (byteIterator.next() != '=') {
throw log.mechInvalidClientMessage(this.mechanismName);
}
authorizationID = byteIterator.delimitedBy(',').asUtf8String().drainToString();
if (byteIterator.next() != ',') {
throw ElytronMessages.log.mechInvalidClientMessage(this.mechanismName);
}
}
}
String auth = getValue("auth", byteIterator.asUtf8String().drainToString());
if (auth == null) {
throw log.mechInvalidClientMessage(this.mechanismName);
}
return new OAuth2InitialClientMessage(authorizationID, auth, messageBytes);
} catch (NoSuchElementException ignored) {
throw ElytronMessages.log.mechInvalidMessageReceived(this.mechanismName);
}
}
private String getValue(String key, String keyValuesPart) {
for (String current : keyValuesPart.split(KV_DELIMITER)) {
String[] keyValue = current.split("=");
if (keyValue[0].equals(key)) {
return keyValue[1];
}
}
return null;
}
public byte[] evaluateInitialResponse(OAuth2InitialClientMessage initialClientMessage) throws AuthenticationMechanismException {
if (initialClientMessage.isBearerToken()) {
String auth = initialClientMessage.getAuth();
String token = auth.substring(auth.indexOf(" ") + 1);
BearerTokenEvidence evidence = new BearerTokenEvidence(token);
EvidenceVerifyCallback evidenceVerifyCallback = new EvidenceVerifyCallback(evidence);
try {
MechanismUtil.handleCallbacks(this.mechanismName, this.callbackHandler, evidenceVerifyCallback);
} catch (UnsupportedCallbackException e) {
throw log.mechAuthorizationUnsupported(this.mechanismName, e);
}
// successful verification, token is supposed to be valid and just respond with an empty message
if (evidenceVerifyCallback.isVerified()) {
AuthorizeCallback authorizeCallback = new AuthorizeCallback(null, null);
try {
MechanismUtil.handleCallbacks(this.mechanismName, this.callbackHandler, authorizeCallback);
} catch (UnsupportedCallbackException e) {
throw log.mechAuthorizationUnsupported(this.mechanismName, e);
}
if (authorizeCallback.isAuthorized()) {
return new byte[0];
}
}
return createErrorMessage();
}
throw log.mechInvalidClientMessage(this.mechanismName);
}
private byte[] createErrorMessage() {
JsonObjectBuilder objectBuilder = Json.createObjectBuilder();
objectBuilder.add("status", "invalid_token");
Object asDiscoveryUrl = serverConfig.get(CONFIG_OPENID_CONFIGURATION_URL);
if (asDiscoveryUrl != null) {
objectBuilder.add(CONFIG_OPENID_CONFIGURATION_URL, asDiscoveryUrl.toString());
}
return ByteIterator.ofBytes(objectBuilder.build().toString().getBytes()).base64Encode().asUtf8().drain();
}
}