zoho self client - new test code
This commit is contained in:
parent
628d10bae9
commit
a756997247
10 changed files with 449 additions and 8 deletions
BIN
doc/images/zoho-001.png
Normal file
BIN
doc/images/zoho-001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
BIN
doc/images/zoho-002.png
Normal file
BIN
doc/images/zoho-002.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
|
|
@ -41,3 +41,50 @@ ee9ba49f16e77c64abb89cea13542d2326d8d03ce8
|
|||
## 3. Generate Access and Refresh Token
|
||||
|
||||
https://accounts.zoho.com/oauth/v2/token?code=1000.dd7exxxxxxxxxxxxxxxxxxxxxxxx9bb8.b6c0xxxxxxxxxxxxxxxxxxxxxxxxdca4&client_id=1000.NLTM2KUEHI696MSLGEIANATVGGX7IN&client_secret=ee9ba49f16e77c64abb89cea13542d2326d8d03ce8&redirect_uri=http://www.zoho.com/books&grant_type=authorization_code
|
||||
|
||||
# Self Client
|
||||
|
||||
Man kann bei zoho mit einem sogenannten 'Self Client' arbeiten. Dieser erlaubt es ohne Redirect auf die Zoho Webseite einen Code anzufordern:
|
||||
|
||||
## 1. Self Client einrichten
|
||||
|
||||
Auf die API COnsole von Zoho zugreifen:
|
||||
|
||||
https://api-console.zoho.eu/client/1000.GR5FTPZYB06HEWSFSSX4DJU3OBD4EJ
|
||||
|
||||
<img src="../images/zoho-001.png" width="800">
|
||||
|
||||
Es kann maximal ein Client eingerichtet werden
|
||||
|
||||
Unter Scope trägt man dann den gewünschten Scope ein. Z.B.:
|
||||
|
||||
ZohoBooks.invoices.CREATE,ZohoBooks.invoices.READ,ZohoBooks.invoices.UPDATE,ZohoBooks.invoices.DELETE
|
||||
|
||||
und eine Description
|
||||
|
||||
<img src="../images/zoho-002.png">
|
||||
|
||||
Den generierten Code nun in Anwendung übertragen und innerhalb (!!) der 'Time Duration' den Access und Refresh Token generieren!
|
||||
|
||||
Siehe Junit Test `TestUpdateTokens`
|
||||
|
||||
Find details also [here](https://www.zoho.com/writer/help/api/v1/oauth-step2.html#self-client)
|
||||
|
||||
## Example for JUnit Test
|
||||
|
||||
Der Junit Test `TestUpdateTokens` zeigt das Prinzip
|
||||
|
||||
```
|
||||
client ID= 1000.GR5FTPZYB06HEWSFSSX4DJU3OBD4EJ
|
||||
client secret= 3f3e74c5212bbc924a7ebf77f887c6296db1106c47
|
||||
```
|
||||
|
||||
In der Web GUi einen neuen Code generieren:
|
||||
|
||||
Scope=ZohoBooks.invoices.CREATE,ZohoBooks.invoices.READ,ZohoBooks.invoices.UPDATE,ZohoBooks.invoices.DELETE
|
||||
|
||||
Danach den Code token kopieren:
|
||||
|
||||
1000.7fe6315e8848b92a2f281b885677d37a.428c02dc2309606c86271842f1f67de4
|
||||
|
||||
**Note:** Der Code muss für Test immer wieder neu erstellt werden, da er nur für einige Minuten gültig ist!
|
||||
|
|
|
|||
BIN
doc/zoho/image.png
Normal file
BIN
doc/zoho/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
|
|
@ -0,0 +1,74 @@
|
|||
package com.alexanderlogistics.zoho;
|
||||
|
||||
/**
|
||||
* Helper class for JSON deserialization
|
||||
*
|
||||
* If the request is successful, you would receive the following:
|
||||
*
|
||||
* <pre>
|
||||
{
|
||||
"access_token": "{access_token}",
|
||||
"refresh_token": "{refresh_token}",
|
||||
"api_domain": "https://www.zohoapis.com",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600
|
||||
}
|
||||
* </pre>
|
||||
*/
|
||||
public class TokenResponse {
|
||||
private String access_token;
|
||||
private String refresh_token;
|
||||
private long expires_in;
|
||||
private String api_domain;
|
||||
private String token_type;
|
||||
private String scope; // Neu hinzugefügt für dein Response-Feld
|
||||
|
||||
// Getter/Setter notwendig für Yasson
|
||||
public String getAccess_token() {
|
||||
return access_token;
|
||||
}
|
||||
|
||||
public void setAccess_token(String access_token) {
|
||||
this.access_token = access_token;
|
||||
}
|
||||
|
||||
public String getRefresh_token() {
|
||||
return refresh_token;
|
||||
}
|
||||
|
||||
public void setRefresh_token(String refresh_token) {
|
||||
this.refresh_token = refresh_token;
|
||||
}
|
||||
|
||||
public long getExpires_in() {
|
||||
return expires_in;
|
||||
}
|
||||
|
||||
public void setExpires_in(long expires_in) {
|
||||
this.expires_in = expires_in;
|
||||
}
|
||||
|
||||
public String getApi_domain() {
|
||||
return api_domain;
|
||||
}
|
||||
|
||||
public void setApi_domain(String api_domain) {
|
||||
this.api_domain = api_domain;
|
||||
}
|
||||
|
||||
public String getToken_type() {
|
||||
return token_type;
|
||||
}
|
||||
|
||||
public void setToken_type(String token_type) {
|
||||
this.token_type = token_type;
|
||||
}
|
||||
|
||||
public String getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
public void setScope(String scope) {
|
||||
this.scope = scope;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.alexanderlogistics.zoho;
|
||||
|
||||
import jakarta.annotation.security.DeclareRoles;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.annotation.security.RunAs;
|
||||
import jakarta.ejb.Lock;
|
||||
import jakarta.ejb.LockType;
|
||||
import jakarta.ejb.Singleton;
|
||||
|
||||
/**
|
||||
* Der ZohoAPIService stellt methoden für den Zugriff auf die Zoho API bereit.
|
||||
* Er handelt auch die Authentifizeirung über einen Access Token. Der
|
||||
* ZohoOAuhtGrantService der dazu dient einen AccessCode von Zoho zu ermitteln
|
||||
* übergibt den code an diesesn Servcie. Er dietn also als Singelton
|
||||
*/
|
||||
@DeclareRoles({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS",
|
||||
"org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
|
||||
"org.imixs.ACCESSLEVEL.MANAGERACCESS" })
|
||||
@RolesAllowed({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS",
|
||||
"org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
|
||||
"org.imixs.ACCESSLEVEL.MANAGERACCESS" })
|
||||
@Singleton
|
||||
@RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS")
|
||||
public class ZohoAPIService {
|
||||
|
||||
private String accessToken;
|
||||
private String clientID;
|
||||
private String clientSecret;
|
||||
|
||||
@Lock(LockType.WRITE) // Standardmäßig ist @Lock(LockType.READ)
|
||||
public void setAccessToken(String token) {
|
||||
this.accessToken = token;
|
||||
}
|
||||
|
||||
@Lock(LockType.READ) // Erlaubt parallele Lesezugriffe
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -32,11 +32,9 @@ import java.util.logging.Logger;
|
|||
|
||||
import org.imixs.workflow.exceptions.QueryException;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.ejb.SessionContext;
|
||||
import jakarta.ejb.EJB;
|
||||
import jakarta.ejb.Stateless;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
|
|
@ -69,11 +67,8 @@ public class ZohoOAuthGrantService {
|
|||
@Context
|
||||
private HttpServletRequest servletRequest;
|
||||
|
||||
@Context
|
||||
HttpServletResponse servletResponse;
|
||||
|
||||
@Resource
|
||||
private SessionContext ctx;
|
||||
@EJB
|
||||
ZohoOAuthManager zohoOAuthManager;
|
||||
|
||||
private static Logger logger = Logger.getLogger(ZohoOAuthGrantService.class.getName());
|
||||
|
||||
|
|
@ -122,6 +117,8 @@ public class ZohoOAuthGrantService {
|
|||
logger.info("│ ├── location=" + location);
|
||||
logger.info("│ ├── accounts-server=" + accountsServer);
|
||||
|
||||
zohoOAuthManager.updateTokens(code);
|
||||
|
||||
return "Zoho Grant Token received!";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
package com.alexanderlogistics.zoho;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.imixs.workflow.exceptions.InvalidAccessException;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.security.RunAs;
|
||||
import jakarta.ejb.Lock;
|
||||
import jakarta.ejb.LockType;
|
||||
import jakarta.ejb.Singleton;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.json.bind.Jsonb;
|
||||
import jakarta.json.bind.JsonbBuilder;
|
||||
|
||||
/**
|
||||
* Der ZohoOAuthManager stellt Methoden bereit um einen Access Token zu
|
||||
* erstellen.
|
||||
*
|
||||
*
|
||||
*/
|
||||
@Singleton
|
||||
@RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS")
|
||||
public class ZohoOAuthManager {
|
||||
|
||||
private static Logger logger = Logger.getLogger(ZohoOAuthManager.class.getName());
|
||||
|
||||
private String refreshToken;
|
||||
private String accessToken;
|
||||
private long accessTokenExpiry; // Timestamp (System.currentTimeMillis() + expires_in*1000)
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "zoho.client.id", defaultValue = "1000.NLTM2KUEHI696MSLGEIANATVGGX7IN")
|
||||
Optional<String> clientID;
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "zoho.client.secret", defaultValue = "ee9ba49f16e77c64abb89cea13542d2326d8d03ce8")
|
||||
Optional<String> clientSecret;
|
||||
|
||||
private HttpClient httpClient;
|
||||
private Jsonb jsonb;
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
this.jsonb = JsonbBuilder.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 3: Generate Access and Refresh Token
|
||||
*
|
||||
* After getting code from the GrantService we make a POST request
|
||||
*
|
||||
* See description: https://www.zoho.com/books/api/v3/oauth/
|
||||
*
|
||||
*
|
||||
* @param code
|
||||
*/
|
||||
@Lock(LockType.WRITE)
|
||||
public void updateTokens(String code) {
|
||||
|
||||
logger.info("├── update Zoho tokens");
|
||||
if (!clientID.isPresent()) {
|
||||
throw new InvalidAccessException("Missing client ID");
|
||||
}
|
||||
if (!clientSecret.isPresent()) {
|
||||
throw new InvalidAccessException("Missing client secret");
|
||||
}
|
||||
try {
|
||||
|
||||
String redirectURI = "https://alexander-logistics-dwc.office-workflow.de/api/zoho/grant";
|
||||
|
||||
// Korrekte URL-Konstruktion mit Query-Parametern
|
||||
// String requestUrl = "https://accounts.zoho.com/oauth/v2/token" +
|
||||
// "?code=" + URLEncoder.encode(code, StandardCharsets.UTF_8) +
|
||||
// "&client_id=" + URLEncoder.encode(clientID.get(), StandardCharsets.UTF_8) +
|
||||
// "&client_secret=" + URLEncoder.encode(clientSecret.get(),
|
||||
// StandardCharsets.UTF_8) +
|
||||
// "&redirect_uri=" + URLEncoder.encode("http://www.zoho.com/books",
|
||||
// StandardCharsets.UTF_8) +
|
||||
// "&grant_type=authorization_code";
|
||||
|
||||
String requestUrl = "https://accounts.zoho.eu/oauth/v2/token" +
|
||||
"?code=" + URLEncoder.encode(code, StandardCharsets.UTF_8) +
|
||||
"&client_id=" + URLEncoder.encode(clientID.get(), StandardCharsets.UTF_8) +
|
||||
"&client_secret=" + URLEncoder.encode(clientSecret.get(), StandardCharsets.UTF_8) +
|
||||
"&redirect_uri=" + URLEncoder.encode(redirectURI, StandardCharsets.UTF_8) +
|
||||
"&grant_type=authorization_code";
|
||||
|
||||
logger.info("│ ├── Request URI=" + requestUrl);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(requestUrl))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.POST(HttpRequest.BodyPublishers.noBody()) // Wichtig: Kein Body!
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response;
|
||||
|
||||
response = httpClient.send(request,
|
||||
HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
logger.info("│ ├── Request statusCode=" + response.statusCode());
|
||||
if (response.statusCode() == 200) {
|
||||
|
||||
// 1. Response-Logging für Debugging
|
||||
logger.info("│ ├── Request statusCode=" + response.body());
|
||||
|
||||
TokenResponse tokenResponse = jsonb.fromJson(response.body(),
|
||||
TokenResponse.class);
|
||||
|
||||
this.accessToken = tokenResponse.getAccess_token();
|
||||
logger.info("│ ├── accessToken=" + accessToken);
|
||||
this.refreshToken = tokenResponse.getRefresh_token();
|
||||
logger.info("│ ├── refreshToken=" + refreshToken);
|
||||
this.accessTokenExpiry = System.currentTimeMillis() +
|
||||
(tokenResponse.getExpires_in() * 1000);
|
||||
|
||||
logger.info("│ ├── expires_in=" + tokenResponse.getExpires_in());
|
||||
} else {
|
||||
throw new RuntimeException("Failed to get tokens: " +
|
||||
response.statusCode() + " - " + response.body());
|
||||
}
|
||||
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new InvalidAccessException("Unable to resolve access tokens: " + e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Lock(LockType.READ)
|
||||
public String getValidAccessToken() {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.alexanderlogistics.zoho;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.imixs.workflow.exceptions.InvalidAccessException;
|
||||
import org.imixs.workflow.exceptions.ModelException;
|
||||
import org.imixs.workflow.exceptions.PluginException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jakarta.json.bind.Jsonb;
|
||||
import jakarta.json.bind.JsonbBuilder;
|
||||
|
||||
/**
|
||||
* Diese Klasse generiert einen access und refresh token anhand eines Code
|
||||
* Tokens
|
||||
*
|
||||
*
|
||||
* @author rsoika
|
||||
*/
|
||||
public class TestUpdateTokens {
|
||||
|
||||
final static String ENCODING = "UTF-8";
|
||||
|
||||
private static Logger logger = Logger.getLogger(TestUpdateTokens.class.getName());
|
||||
|
||||
private HttpClient httpClient;
|
||||
private Jsonb jsonb;
|
||||
|
||||
private String code = "1000.a6cff79178a76099a625eb41249a6c91.b3679ff9365902502d2e3ca55303d5d6";
|
||||
|
||||
private String clientID = "1000.GR5FTPZYB06HEWSFSSX4DJU3OBD4EJ";
|
||||
private String clientSecret = "3f3e74c5212bbc924a7ebf77f887c6296db1106c47";
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws PluginException, ModelException {
|
||||
logger.info("setup test environment ...");
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
this.jsonb = JsonbBuilder.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Testet den Vorgang einen Access und Refresh Token zu generieren
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
@Test
|
||||
public void testUpdateTokens() throws IOException {
|
||||
|
||||
logger.info("├── update Zoho tokens");
|
||||
|
||||
try {
|
||||
|
||||
String redirectURI = "https://alexander-logistics-dwc.office-workflow.de/api/zoho/grant";
|
||||
|
||||
String requestUrl = "https://accounts.zoho.eu/oauth/v2/token" +
|
||||
"?code=" + URLEncoder.encode(code, StandardCharsets.UTF_8) +
|
||||
"&client_id=" + URLEncoder.encode(clientID, StandardCharsets.UTF_8) +
|
||||
"&client_secret=" + URLEncoder.encode(clientSecret, StandardCharsets.UTF_8) +
|
||||
"&redirect_uri=" + URLEncoder.encode(redirectURI, StandardCharsets.UTF_8) +
|
||||
"&grant_type=authorization_code";
|
||||
|
||||
logger.info("│ ├── Request URI=" + requestUrl);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(requestUrl))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.POST(HttpRequest.BodyPublishers.noBody()) // Wichtig: Kein Body!
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response;
|
||||
|
||||
response = httpClient.send(request,
|
||||
HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
logger.info("│ ├── Request statusCode=" + response.statusCode());
|
||||
if (response.statusCode() == 200) {
|
||||
|
||||
// 1. Response-Logging für Debugging
|
||||
logger.info("│ ├── Request statusCode=" + response.body());
|
||||
|
||||
TokenResponse tokenResponse = jsonb.fromJson(response.body(),
|
||||
TokenResponse.class);
|
||||
|
||||
String accessToken = tokenResponse.getAccess_token();
|
||||
logger.info("│ ├── accessToken=" + accessToken);
|
||||
String refreshToken = tokenResponse.getRefresh_token();
|
||||
logger.info("│ ├── refreshToken=" + refreshToken);
|
||||
logger.info("│ ├── expires_in=" + tokenResponse.getExpires_in());
|
||||
} else {
|
||||
throw new RuntimeException("Failed to get tokens: " +
|
||||
response.statusCode() + " - " + response.body());
|
||||
}
|
||||
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new InvalidAccessException("Unable to resolve access tokens: " + e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
23
pom.xml
23
pom.xml
|
|
@ -326,5 +326,28 @@
|
|||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON-B Implementation for Tests -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse</groupId>
|
||||
<artifactId>yasson</artifactId>
|
||||
<version>3.0.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish</groupId>
|
||||
<artifactId>jakarta.json</artifactId>
|
||||
<version>2.0.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- HTTP Client -->
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
<version>5.2.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
Loading…
Reference in a new issue