gam-oauth-authenticator
v1.1.2
Published
Headless OAuth 2.0 client for GeneXus GAM Identity Provider
Maintainers
Readme
GAM OAuth Authenticator
Um módulo Node.js headless e independente de framework para integração OAuth 2.0 com GeneXus GAM (GeneXus Access Manager) como Identity Provider.
Características
- ✅ Headless - sem dependências de frameworks HTTP (Express, Next.js, Fastify, etc.)
- ✅ TypeScript nativo com tipos completos
- ✅ Compatível com Node.js 20+ (usa
fetchnativo) - ✅ Suporte a PKCE (Proof Key for Code Exchange)
- ✅ Totalmente testável e configurável
- ✅ Tratamento de erros explícito
- ✅ Single Logout (SLO) – URL de logout único com o GAM
Instalação
npm install gam-oauth-authenticatorRequisitos
- Node.js 20.0.0 ou superior
Uso Básico
Criar o Client
import { createGamOAuthClient } from "gam-oauth-authenticator";
const gamClient = createGamOAuthClient({
baseUrl: "https://idp.exemplo.com/VirtualDir",
clientId: "my-client-id",
clientSecret: "my-client-secret",
defaultScopes: ["gam_user_data"],
});1. Obter access token (por credenciais)
Alternativa ao fluxo OAuth com redirecionamento: obtenha um Bearer token diretamente com usuário e senha (endpoint mobile). O mesmo accessToken retornado é usado em getUserInfo, refreshToken e logoutWithAccessToken.
try {
const tokens = await gamClient.getAccessTokenWithCredentials({
username: "usuario",
password: "senha",
scope: "gam_user_data", // opcional
grantType: "GAMLocal", // opcional, padrão GAMLocal
// repository: "MeuRepo", // opcional, para GAM multi-repositório
});
console.log(tokens.accessToken); // Bearer token
console.log(tokens.refreshToken); // pode ser undefined
console.log(tokens.expiresIn); // em segundos
console.log(tokens.tokenType); // "Bearer"
} catch (error) {
if (error instanceof GamNeedsSecondFactorError) {
// GAM pediu segundo fator (TOTP): chame novamente com password = código
} else if (error instanceof GamNeedsOtpError) {
// GAM enviou OTP por e-mail: chame novamente com password = código
} else if (error instanceof GamTokenError) {
console.error("Erro ao obter token:", error.message);
}
}Se o GAM exigir 2FA ou OTP por e-mail, a primeira chamada retorna erro (202); use o código recebido e chame getAccessTokenWithCredentials de novo com password: codigo e, no 2FA, additionalParameters com OTPStep: "2" e UseTwoFactorAuthentication: "true".
2. Construir URL de Login (fluxo OAuth)
const signinUrl = gamClient.buildSigninUrl({
redirectUri: "https://myapp.com/callback",
state: "random-state-string",
scopes: ["gam_user_data"], // opcional, usa defaultScopes se não fornecido
});
// Redirecionar o usuário para signinUrl3. Trocar Código por Token (fluxo OAuth)
try {
const tokens = await gamClient.exchangeCodeForToken({
code: "authorization-code-from-callback",
redirectUri: "https://myapp.com/callback", // deve corresponder exatamente
});
console.log(tokens.accessToken);
console.log(tokens.refreshToken); // pode ser undefined
console.log(tokens.expiresIn); // em segundos
} catch (error) {
if (error instanceof GamTokenError) {
console.error("Erro OAuth:", error.error, error.errorDescription);
}
}4. Obter Informações do Usuário
try {
const userInfo = await gamClient.getUserInfo(tokens.accessToken);
console.log(userInfo.guid); // ID do usuário (GAM GUID)
console.log(userInfo.name);
console.log(userInfo.email);
console.log(userInfo.roles);
} catch (error) {
if (error instanceof GamUserInfoError) {
console.error("Erro ao buscar informações do usuário:", error.message);
}
}5. Renovar Token (Opcional)
try {
const newTokens = await gamClient.refreshToken(tokens.refreshToken);
console.log(newTokens.accessToken);
console.log(newTokens.refreshToken); // novo refresh token (pode ser undefined)
} catch (error) {
if (error instanceof GamTokenError) {
console.error("Erro ao renovar token:", error.message);
}
}6. Single Logout (Logout único)
Para encerrar a sessão no GAM e redirecionar o usuário de volta à sua aplicação, use a URL de signout. O GAM invalida a sessão do usuário no IdP e redireciona para o redirectUri informado. Recomenda-se validar o parâmetro state no callback (igual ao fluxo de login) para proteção CSRF.
import crypto from "crypto";
// 1. Gerar e guardar o state (ex.: em cookie ou sessão)
const state = crypto.randomBytes(32).toString("hex");
// 2. Construir a URL de logout e redirecionar o usuário
const signoutUrl = gamClient.buildSignoutUrl({
redirectUri: "https://myapp.com/auth/logout/callback",
token: tokens.accessToken, // access token da sessão atual
state,
});
// Redirecionar o usuário para signoutUrl
// window.location.href = signoutUrl; (no browser)
// res.redirect(signoutUrl); (no Express, etc.)No callback (/auth/logout/callback), valide que o state recebido na query corresponde ao que foi guardado; em seguida, limpe a sessão/local storage da sua aplicação e redirecione para a página inicial ou login.
7. Logout para token mobile (access token)
Para tokens obtidos via login por credenciais (getAccessTokenWithCredentials), use o endpoint de logout mobile para invalidar o access token no GAM sem redirecionamento. O serviço REST recebe o token no header Authorization e invalida a sessão no servidor.
Endpoint utilizado: POST /oauth/logout (baseUrl + path).
try {
const result = await gamClient.logoutWithAccessToken(tokens.accessToken);
if (result.code === "200") {
// Logout concluído no GAM; limpe a sessão local (cookie, storage, etc.)
// e redirecione ou responda ao cliente
}
} catch (error) {
if (error instanceof GamTokenError) {
console.error("Erro ao invalidar token:", error.message);
} else if (error instanceof GamNetworkError) {
console.error("Erro de rede no logout:", error.message);
}
}Em aplicações BFF: ao receber uma requisição de logout do cliente, chame logoutWithAccessToken(accessToken) com o token da sessão atual e, em seguida, remova o cookie ou a sessão no seu backend.
Documentação oficial: HowTo: Sign out from mobile applications using GAM.
Uso com PKCE
import { createGamOAuthClient } from "gam-oauth-authenticator";
import crypto from "crypto";
// Gerar code verifier e challenge (você precisa implementar isso)
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto
.createHash("sha256")
.update(codeVerifier)
.digest("base64url");
// Construir URL com PKCE
const signinUrl = gamClient.buildSigninUrl({
redirectUri: "https://myapp.com/callback",
state: "random-state-string",
pkce: {
codeChallenge,
method: "S256",
},
});
// Ao trocar o código, incluir o code verifier
const tokens = await gamClient.exchangeCodeForToken({
code: "authorization-code",
redirectUri: "https://myapp.com/callback",
codeVerifier, // necessário quando PKCE foi usado
});Tratamento de Erros
O módulo exporta classes de erro específicas:
import {
GamOAuthError,
GamTokenError,
GamUserInfoError,
GamNetworkError,
GamConfigurationError,
} from "gam-oauth-authenticator";
try {
// ... operação OAuth
} catch (error) {
if (error instanceof GamTokenError) {
// Erro no token endpoint
console.error(error.error); // código do erro OAuth
console.error(error.errorDescription); // descrição do erro
console.error(error.statusCode); // código HTTP
} else if (error instanceof GamUserInfoError) {
// Erro no userinfo endpoint
console.error(error.statusCode);
console.error(error.responseBody);
} else if (error instanceof GamNetworkError) {
// Erro de rede
console.error(error.originalError);
} else if (error instanceof GamConfigurationError) {
// Erro de configuração
console.error(error.message);
}
}API Reference
createGamOAuthClient(config)
Cria uma instância do client OAuth GAM.
Parâmetros:
config.baseUrl(string, obrigatório): URL base do Identity Provider GAMconfig.clientId(string, obrigatório): ID do cliente OAuthconfig.clientSecret(string, obrigatório): Secret do cliente OAuthconfig.defaultScopes(string[], opcional): Escopos padrão (padrão:["gam_user_data"])config.fetch(Function, opcional): Implementação customizada de fetch (útil para testes)
Retorna: GamOAuthClient
GamOAuthClient.buildSigninUrl(options)
Constrói a URL de autorização para login.
Parâmetros:
options.redirectUri(string, obrigatório): URI de redirecionamentooptions.state(string, obrigatório): Parâmetro state para proteção CSRFoptions.scopes(string[], opcional): Escopos a solicitaroptions.pkce(object, opcional): Configuração PKCEpkce.codeChallenge(string): Code challengepkce.method("S256"): Método do challenge
Retorna: string - URL completa de autorização
GamOAuthClient.buildSignoutUrl(options)
Constrói a URL de signout (single logout) no GAM. O usuário deve ser redirecionado para essa URL; o GAM encerra a sessão e redireciona para redirectUri. O state deve ser guardado (ex.: cookie) e validado no callback.
Parâmetros:
options.redirectUri(string, obrigatório): URI para onde o usuário será redirecionado após o logout no GAMoptions.token(string, obrigatório): Access token da sessão atual (enviado ao GAM para invalidar a sessão)options.state(string, obrigatório): Valor de state para proteção CSRF (guardar e validar no callback)
Retorna: string - URL completa de signout
GamOAuthClient.exchangeCodeForToken(options)
Troca um código de autorização por tokens.
Parâmetros:
options.code(string, obrigatório): Código de autorizaçãooptions.redirectUri(string, obrigatório): URI de redirecionamento usado na autorizaçãooptions.codeVerifier(string, opcional): Code verifier para PKCE
Retorna: Promise<TokenResponse>
GamOAuthClient.getAccessTokenWithCredentials(options)
Obtém um access token (Bearer) diretamente com usuário e senha, sem fluxo OAuth. Endpoint mobile: POST /oauth/access_token. Útil para aplicações que autenticam no backend (ex.: BFF). Suporta 2FA e OTP por e-mail (segundo passo com código).
Parâmetros:
options.username(string, obrigatório): Nome de usuáriooptions.password(string, obrigatório): Senha (ou código OTP/TOTP no segundo passo)options.scope(string, opcional): Escopo (padrão: valor de defaultScopes do client, ex."gam_user_data")options.grantType(string, opcional): Tipo de grant (padrão:"GAMLocal")options.repository(string, opcional): Nome do repositório GAM (enviado emadditional_parameterscomoRepository)options.additionalParameters(object, opcional): Parâmetros extras no body (ex. 2FA:OTPStep: "2",UseTwoFactorAuthentication: "true")
Retorna: Promise<TokenResponse> – accessToken, refreshToken, expiresIn, tokenType.
Erros: Em 202 com código 410 lança GamNeedsSecondFactorError (pedir TOTP); em 202 com código 400 lança GamNeedsOtpError (pedir código por e-mail). Chame novamente com password: codigo e, no 2FA, additionalParameters adequados.
GamOAuthClient.getUserInfo(accessToken)
Obtém informações do usuário autenticado.
Parâmetros:
accessToken(string, obrigatório): Access token
Retorna: Promise<UserInfoResponse>
GamOAuthClient.refreshToken(refreshToken, options?)
Renova um access token usando um refresh token.
Parâmetros:
refreshToken(string, obrigatório): Refresh tokenoptions.useMobileEndpoint(boolean, opcional): Setrue, usa o endpoint/oauth/access_token(para tokens obtidos viagetAccessTokenWithCredentials)
Retorna: Promise<RefreshTokenResponse>
GamOAuthClient.logoutWithAccessToken(accessToken)
Invalida o access token no servidor GAM (fluxo mobile; não redireciona o usuário). Use para tokens obtidos via getAccessTokenWithCredentials.
Parâmetros:
accessToken(string, obrigatório): Access token obtido no login mobile (credenciais)
Retorna: Promise<LogoutResponse> – objeto com code (string); sucesso quando code === "200".
Endpoints GAM
O módulo usa os seguintes endpoints do GAM:
- Authorization:
/oauth/gam/signin - Token (Web):
/oauth/gam/access_token - Token (mobile/credenciais):
/oauth/access_token - Logout mobile:
POST /oauth/logout– invalida o access token no GAM (uso com token obtido via credenciais) - UserInfo:
/oauth/gam/userinfo - Signout (Single Logout):
/oauth/gam/signout
Documentação oficial:
