package com.perforce.workspace.tjuricek.p4oauth.services; import com.perforce.p4java.exception.P4JavaException; import com.perforce.p4java.option.server.LoginOptions; import com.perforce.p4java.server.IOptionsServer; import com.perforce.workspace.tjuricek.p4oauth.models.AccessToken; import com.perforce.workspace.tjuricek.p4oauth.models.AuthorizationCode; import com.perforce.workspace.tjuricek.p4oauth.models.Token; import com.perforce.workspace.tjuricek.p4oauth.models.Whitelist; import com.perforce.workspace.tjuricek.p4oauth.storage.AuthorizationCodeStore; import com.perforce.workspace.tjuricek.p4oauth.storage.WhitelistStore; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.IOException; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Optional; import java.util.function.Supplier; import static com.perforce.workspace.tjuricek.p4oauth.services.AccessTokenResult.AccessTokenError.*; /** */ public class CodeService { private static final Logger logger = LogManager.getFormatterLogger(CodeService.class); private Supplier<WhitelistStore> whitelistStore; private Supplier<IOptionsServer> optionsServerSupplier; private Supplier<AuthorizationCodeStore> authorizationCodeStoreSupplier; public CodeService(Supplier<WhitelistStore> whitelistStore, Supplier<IOptionsServer> optionsServerSupplier, Supplier<AuthorizationCodeStore> authorizationCodeStoreSupplier) { this.whitelistStore = whitelistStore; this.optionsServerSupplier = optionsServerSupplier; this.authorizationCodeStoreSupplier = authorizationCodeStoreSupplier; } /** * Generates an authorization code for the user, appropriate for the * redirectUrl. * <p> * The server MUST have the redirectUrl in it's whitelist. When we * run this method, we simply make sure that the redirect URL is valid, * and then generate a new "request code" that we use in a redirect to * our local login screen. * * @param redirectUrl * @return if an error occurs, will be non-null. Should only return the * "not whitelisted" failure, which probably is a 400 error */ public CodeError startCodeRequest(String redirectUrl) throws IOException { if (!whitelistStore.get().read().isWhitelisted(redirectUrl)) { return CodeError.NotWhitelisted; } return null; } /** * Handles local log-in from the user. If it's successful, we generate the * authorization code to redirect back to the resource owner. * * @param login Perforce login * @param password Perforce password * @param redirectUrl The URI we'll redirect back to, required to be * whitelisted * * @return A result that either indicates the error or the new authorization * code. * * @throws IOException */ public CodeResult authenticateCodeRequest(String login, String password, String redirectUrl) throws IOException { Whitelist whitelist = whitelistStore.get().read(); if (!whitelist.isWhitelisted(redirectUrl)) { return CodeResult.error(CodeError.NotWhitelisted); } // Right now, just attempt to login try { IOptionsServer optionsServer = optionsServerSupplier.get(); if (!optionsServer.isConnected()) { optionsServer.connect(); } optionsServer.setUserName(login); optionsServer.login(password); optionsServer.logout(); } catch (P4JavaException p4Ex) { logger.info("interpreting exception as login failure", p4Ex); return CodeResult.error(CodeError.AuthenticationFailed); } AuthorizationCode code = AuthorizationCode.create(redirectUrl, login); // Generate a token for each server in the whitelist. These get stored // and if we get a valid access code for the user, we'll send these // tokens back for the associated redirect. // // We tend to just "regenerate" tokens. IOptionsServer optionsServer = optionsServerSupplier.get(); optionsServer.setUserName(login); whitelist.getServers().forEach(server -> { // Set up our options to print out the ticket LoginOptions options = new LoginOptions(false, true, server.getIpAddress()); StringBuffer sb = new StringBuffer(); try { optionsServer.login(password, sb, options); } catch (P4JavaException e) { throw new IllegalStateException(e); } Token token = new Token(server.getIpAddress(), redirectUrl, sb.toString()); code.addToken(token); }); authorizationCodeStoreSupplier.get().update(code); return CodeResult.code(code.getAuthorizationCode()); } public AccessTokenResult createAccessToken(String login, String redirectUri, String code) throws IOException { Optional<AuthorizationCode> authCodeOpt = authorizationCodeStoreSupplier.get().read(code); if (!authCodeOpt.isPresent()) { return AccessTokenResult.error(AuthCodeNotFound); } AuthorizationCode authCode = authCodeOpt.get(); if (!authCode.getClientId().equals(login) || !authCode.getRedirectUri().equals(redirectUri)) { return AccessTokenResult.error(IllegalParameters); } Instant createdInstant = Instant.ofEpochMilli(authCode.getCreated()); if (createdInstant.isBefore(Instant.now().minus(10, ChronoUnit.MINUTES))) { return AccessTokenResult.error(AuthCodeOutOfDate); } // OK, at this point, seems valid, so generate the new access token. AccessToken accessToken = AccessToken.forAuthCode(authCode); Optional<Token> tokenOpt = accessToken.findToken(redirectUri); if (!tokenOpt.isPresent()) { return AccessTokenResult.error(NoPerforceToken); } // We're done, so just ensure the next request for this isn't possible authorizationCodeStoreSupplier.get().delete(code); return AccessTokenResult.success(accessToken.getId(), tokenOpt.get().getToken()); } }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#2 | 9095 | tjuricek | Added some basic test data and renamed "workspace" to "workshop" in package name | ||
#1 | 9089 | tjuricek |
Moving some code that worked via some manual validation to the workshop. This just implements a basic code authorization grant scheme. Automated tests are forthcoming, awating some gradle plugin work that should sit outside of this project. |