package org.jetbrains.teamcity.aad;
import com.intellij.openapi.util.text.StringUtil;
import jetbrains.buildServer.controllers.interceptors.auth.HttpAuthenticationProtocol;
import jetbrains.buildServer.controllers.interceptors.auth.HttpAuthenticationResult;
import jetbrains.buildServer.controllers.interceptors.auth.HttpAuthenticationSchemeAdapter;
import jetbrains.buildServer.controllers.interceptors.auth.util.HttpAuthUtil;
import jetbrains.buildServer.serverSide.auth.LoginConfiguration;
import jetbrains.buildServer.serverSide.auth.ServerPrincipal;
import jetbrains.buildServer.web.openapi.PluginDescriptor;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.http.HttpStatus;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
/**
* @author Evgeniy.Koshkin
*/
public class AADAuthenticationScheme extends HttpAuthenticationSchemeAdapter {
private static final Logger LOG = Logger.getLogger(AADAuthenticationScheme.class);
private static final String POST_METHOD = "POST";
private static final String ID_TOKEN = "id_token";
private static final String NONCE_CLAIM = "nonce";
private static final String NAME_CLAIM = "unique_name";
private static final String OID_CLAIM = "oid"; //object ID
private static final String EMAIL_CLAIM = "upn";
private static final String ERROR_CLAIM = "error";
private static final String ERROR_DESCRIPTION_CLAIM = "error_description";
@NotNull private final PluginDescriptor myPluginDescriptor;
@NotNull private final ServerPrincipalFactory myPrincipalFactory;
public AADAuthenticationScheme(@NotNull final LoginConfiguration loginConfiguration,
@NotNull final PluginDescriptor pluginDescriptor,
@NotNull final ServerPrincipalFactory principalFactory) {
myPluginDescriptor = pluginDescriptor;
myPrincipalFactory = principalFactory;
loginConfiguration.registerAuthModuleType(this);
}
@NotNull
@Override
protected String doGetName() {
return AADConstants.AAD_AUTH_SCHEME_NAME;
}
@NotNull
@Override
public String getDisplayName() {
return "Microsoft Azure Active Directory";
}
@NotNull
@Override
public String getDescription() {
return "Authentication via Microsoft Azure Active Directory";
}
@Nullable
@Override
public String getEditPropertiesJspFilePath() {
return myPluginDescriptor.getPluginResourcesPath("editAADSchemeProperties.jsp");
}
@Nullable
@Override
public Collection<String> validate(@NotNull Map<String, String> properties) {
final Collection<String> errors = new ArrayList<String>();
if(StringUtil.isEmptyOrSpaces(properties.get(AADConstants.AUTH_ENDPOINT_SCHEME_PROPERTY_KEY))){
errors.add("App OAuth 2.0 authorization endpoint should be specified.");
}
if(StringUtil.isEmptyOrSpaces(properties.get(AADConstants.CLIENT_ID_SCHEME_PROPERTY_KEY))){
errors.add("Client ID should be specified.");
}
return errors.isEmpty() ? super.validate(properties) : errors;
}
@NotNull
@Override
public HttpAuthenticationResult processAuthenticationRequest(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Map<String, String> schemeProperties) throws IOException {
if (!request.getMethod().equals(POST_METHOD)) return HttpAuthenticationResult.notApplicable();
final String idTokenString = request.getParameter(ID_TOKEN);
if(idTokenString == null){
LOG.debug("POST request contains no " + ID_TOKEN + " parameter so scheme is not applicable.");
return HttpAuthenticationResult.notApplicable();
}
final JWT token = JWT.parse(idTokenString);
if(token == null)
return sendBadRequest(response, String.format("Marked request as unauthenticated since failed to parse JWT from retrieved %s %s", ID_TOKEN, idTokenString));
final String error = token.getClaim(ERROR_CLAIM);
final String errorDescription = token.getClaim(ERROR_DESCRIPTION_CLAIM);
if(error != null){
LOG.warn(error);
return sendUnauthorized(request, response, errorDescription);
}
final String nonce = token.getClaim(NONCE_CLAIM);
final String name = token.getClaim(NAME_CLAIM);
final String oid = token.getClaim(OID_CLAIM);
if (nonce == null || name == null || oid == null)
return sendBadRequest(response, String.format("Some of required claims were not found in parsed JWT. nonce - %s; name - %s, oid - %s", nonce, name, oid));
if(!nonce.equals(SessionUtil.getSessionId(request)))
return sendBadRequest(response, "Marked request as unauthenticated since retrieved JWT 'nonce' claim doesn't correspond to current TeamCity session.");
final String email = token.getClaim(EMAIL_CLAIM);
final ServerPrincipal principal = myPrincipalFactory.getServerPrincipal(name, oid, email, schemeProperties);
LOG.debug("Request authenticated. Determined user " + principal.getName());
return HttpAuthenticationResult.authenticated(principal, HttpAuthRememberMeUtil.mustRememberMe());
}
private HttpAuthenticationResult sendUnauthorized(HttpServletRequest request, HttpServletResponse response, String reason) throws IOException {
return HttpAuthUtil.sendUnauthorized(request, response, reason, Collections.<HttpAuthenticationProtocol>emptySet());
}
private HttpAuthenticationResult sendBadRequest(HttpServletResponse response, String reason) throws IOException {
LOG.warn(reason);
response.sendError(HttpStatus.BAD_REQUEST.value(), reason);
return HttpAuthenticationResult.unauthenticated();
}
}