How to use OAuth Authorization Code for CLIs - authentication

Trying to allow a CLI I'm developing to "login" via web browser and obtain an access token for the user's account, similar to how gcloud and github's CLIs do it. I realize it'll be using the OAuth Authorization Code flow.
But what about the client_secret?
I've found out that github cli just doesn't care about exposing it, and it's right there in the source code: https://github.com/cli/cli/blob/6a8deb1f5a9f2aa0ace2eb154523f3b9f23a05ae/internal/authflow/flow.go#L25-L26
Why is this not a problem? or is it?
I'm not yet using OAuth for the cli's login

STANDARDS
The CLI app is a native public client and should use authorization code flow + PKCE rather than a fixed client secret. It should also follow the flow from
RFC8252 and receive the browser response using a local HTTP (loopback) URI.
THIS IMPLEMENTATION
Looks like the github code here uses a client secret and does not use PKCE. You may have to provide a client secret if using this library, but it cannot be kept secret from users. Any user could easily view it, eg with an HTTP proxy tool.
CODE
If the infrastructure enables you to follow the standards, aim for something similar to this Node.js code.
* The OAuth flow for a console app
*/
export async function login(): Promise<string> {
// Set up the authorization request
const codeVerifier = generateRandomString();
const codeChallenge = generateHash(codeVerifier);
const state = generateRandomString();
const authorizationUrl = buildAuthorizationUrl(state, codeChallenge);
return new Promise<string>((resolve, reject) => {
let server: Http.Server | null = null;
const callback = async (request: Http.IncomingMessage, response: Http.ServerResponse) => {
if (server != null) {
// Complete the incoming HTTP request when a login response is received
response.write('Login completed for the console client ...');
response.end();
server.close();
server = null;
try {
// Swap the code for tokens
const accessToken = await redeemCodeForAccessToken(request.url!, state, codeVerifier);
resolve(accessToken);
} catch (e: any) {
reject(e);
}
}
}
// Start an HTTP server and listen for the authorization response on a loopback URL, according to RFC8252
server = Http.createServer(callback);
server.listen(loopbackPort);
// Open the system browser to begin authentication
Opener(authorizationUrl);
});
}
/*
* Build a code flow URL for a native console app
*/
function buildAuthorizationUrl(state: string, codeChallenge: string): string {
let url = authorizationEndpoint;
url += `?client_id=${encodeURIComponent(clientId)}`;
url += `&redirect_uri=${encodeURIComponent(redirectUri)}`;
url += '&response_type=code';
url += `&scope=${scope}`;
url += `&state=${encodeURIComponent(state)}`;
url += `&code_challenge=${encodeURIComponent(codeChallenge)}`;
url += '&code_challenge_method=S256';
return url;
}
/*
* Swap the code for tokens using PKCE and return the access token
*/
async function redeemCodeForAccessToken(responseUrl: string, requestState: string, codeVerifier: string): Promise<string> {
const [code, responseState] = getLoginResult(responseUrl);
if (responseState !== requestState) {
throw new Error('An invalid authorization response state was received');
}
let body = 'grant_type=authorization_code';
body += `&client_id=${encodeURIComponent(clientId)}`;
body += `&redirect_uri=${encodeURIComponent(redirectUri)}`;
body += `&code=${encodeURIComponent(code)}`;
body += `&code_verifier=${encodeURIComponent(codeVerifier)}`;
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body,
};
const response = await fetch(tokenEndpoint, options);
if (response.status >= 400) {
const details = await response.text();
throw new Error(`Problem encountered redeeming the code for tokens: ${response.status}, ${details}`);
}
const data = await response.json();
return data.access_token;
}

Related

How to debug invalid_grant during authorization code exchange?

My application is using OAuth to access the Youtube Data API. My OAuth callback is written in node and uses the OAuth2Client class from the "googleapis" npm package to exchange the authorization code for the access and refresh tokens.
Everything was working fine up to last week until suddenly I started getting the "invalid_grant" response during the authorization code exchange. I have tried everything to resolve this and am running out of ideas. My callback executes as a cloud function so I don't think that it would be out of sync with NTP.
My OAuth consent screen is in "Testing" mode and my email address is included in the test users. The odd thing is that even though the authorization code exchange fails, my Google account's "Third-party apps with account access" section lists my application as if the handshake succeeded.
Here is my code for generating the auth URL and exchanging the authorization code. The invalid_grant occurs during the call to "oauth2.getToken"
async startFlow(scopes: string[], state: string): Promise<AuthFlow> {
const codes = await oauth2.generateCodeVerifierAsync();
const href = oauth2.generateAuthUrl({
scope: scopes,
state,
access_type: 'offline',
include_granted_scopes: true,
prompt: 'consent',
code_challenge_method: CodeChallengeMethod.S256,
code_challenge: codes.codeChallenge
});
return { href, code_verifier: codes.codeVerifier };
}
async finishFlow(code: string, verifier: string): Promise<Tokens> {
const tokens = await oauth2.getToken({ code, codeVerifier: verifier })
return {
refresh_token: tokens.tokens.refresh_token!,
access_token: tokens.tokens.access_token!,
expires_in: tokens.tokens.expiry_date!,
token_type: 'Bearer',
scopes: tokens.tokens.scope!.split(' ')
};
}
"oauth2" is an instance of OAuth2Client from "google-auth-library". I initialize it here:
export const oauth2 = new google.auth.OAuth2({
clientId: YT_CLIENT_ID,
clientSecret: YT_CLIENT_SECRET,
redirectUri: `${APP_URI}/oauth`
});
Looking at the logs, the only out of the ordinary thing I notice is that the application/x-www-form-urlencoded body looks slightly different than the example https://developers.google.com/identity/protocols/oauth2/web-server#exchange-authorization-code
The POST request to "https://oauth2.googleapis.com/token" ends up looking like this:
code=4%2F0AX4XfWiKHVnsavUH7en0TywjPJVRyJ9aGN-JR8CAAcAG7dT-THxyWQNcxd769nzaHLUb8Q&client_id=XXXXXXXXXX-XXXXXXXXXXXXXXX.apps.googleusercontent.com&client_secret=XXXXXX-XXXXXXXXXXXXXXX-XX_XXX&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth&grant_type=authorization_code&code_verifier=KjOBmr4D9ISLPSE4claEBWr3UN-bKdPHZa8BBcQvcmajfr9RhWrgt7G429PLEpsP7oGzFGnBICu3HgWaHPsLhMkGBuQ2GmHHiB4OpY2F0rJ06wkpCjV2cCTDdpfRY~Ej
Notice that the "/" characters are not percent-encoded in the official example, but they are in my requests. Could this actually be the issue? I don't see how the official google auth library would have an issue this large.
If you check the documentation on expiration of refresh tokens you will see
A Google Cloud Platform project with an OAuth consent screen configured for an external user type and a publishing status of "Testing" is issued a refresh token expiring in 7 days.
Set your project to production mode and your refresh tokens will last longer then a week.
library
IMO if your refresh token expires your library should be requesting access again. However the library was created before the change that a refresh token gets revoked. This isnt a true refresh token expired error message and the library isn't detecting that.
A post over on the issue forum for your library may encourage them to patch it so that it is more obvious of an error message.
At any rate you need to delete the stored tokens for that user which will request authorization again.
update If its not that.
ensure that your computer / server has the correct time
revoke the users access directly via their google account. Under third party apps with access ensure that your app isnt listed if it is remove it.
Make sure that you are using the correct client type from google developer console.
Google node.js sample
var fs = require('fs');
var readline = require('readline');
var {google} = require('googleapis');
var OAuth2 = google.auth.OAuth2;
// If modifying these scopes, delete your previously saved credentials
// at ~/.credentials/youtube-nodejs-quickstart.json
var SCOPES = ['https://www.googleapis.com/auth/youtube.readonly'];
var TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH ||
process.env.USERPROFILE) + '/.credentials/';
var TOKEN_PATH = TOKEN_DIR + 'youtube-nodejs-quickstart.json';
// Load client secrets from a local file.
fs.readFile('client_secret.json', function processClientSecrets(err, content) {
if (err) {
console.log('Error loading client secret file: ' + err);
return;
}
// Authorize a client with the loaded credentials, then call the YouTube API.
authorize(JSON.parse(content), getChannel);
});
/**
* Create an OAuth2 client with the given credentials, and then execute the
* given callback function.
*
* #param {Object} credentials The authorization client credentials.
* #param {function} callback The callback to call with the authorized client.
*/
function authorize(credentials, callback) {
var clientSecret = credentials.installed.client_secret;
var clientId = credentials.installed.client_id;
var redirectUrl = credentials.installed.redirect_uris[0];
var oauth2Client = new OAuth2(clientId, clientSecret, redirectUrl);
// Check if we have previously stored a token.
fs.readFile(TOKEN_PATH, function(err, token) {
if (err) {
getNewToken(oauth2Client, callback);
} else {
oauth2Client.credentials = JSON.parse(token);
callback(oauth2Client);
}
});
}
/**
* Get and store new token after prompting for user authorization, and then
* execute the given callback with the authorized OAuth2 client.
*
* #param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
* #param {getEventsCallback} callback The callback to call with the authorized
* client.
*/
function getNewToken(oauth2Client, callback) {
var authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES
});
console.log('Authorize this app by visiting this url: ', authUrl);
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('Enter the code from that page here: ', function(code) {
rl.close();
oauth2Client.getToken(code, function(err, token) {
if (err) {
console.log('Error while trying to retrieve access token', err);
return;
}
oauth2Client.credentials = token;
storeToken(token);
callback(oauth2Client);
});
});
}
/**
* Store token to disk be used in later program executions.
*
* #param {Object} token The token to store to disk.
*/
function storeToken(token) {
try {
fs.mkdirSync(TOKEN_DIR);
} catch (err) {
if (err.code != 'EEXIST') {
throw err;
}
}
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
if (err) throw err;
console.log('Token stored to ' + TOKEN_PATH);
});
}
/**
* Lists the names and IDs of up to 10 files.
*
* #param {google.auth.OAuth2} auth An authorized OAuth2 client.
*/
function getChannel(auth) {
var service = google.youtube('v3');
service.channels.list({
auth: auth,
part: 'snippet,contentDetails,statistics',
forUsername: 'GoogleDevelopers'
}, function(err, response) {
if (err) {
console.log('The API returned an error: ' + err);
return;
}
var channels = response.data.items;
if (channels.length == 0) {
console.log('No channel found.');
} else {
console.log('This channel\'s ID is %s. Its title is \'%s\', and ' +
'it has %s views.',
channels[0].id,
channels[0].snippet.title,
channels[0].statistics.viewCount);
}
});
}
I've discovered my problem. Another part of my code was throwing an error while the authorization request was being sent (some weird async code). This caused the request to bail halfway through and send only a partial body. This resulted in the invalid_grant appearing in my logs which misled me to believe this was the root cause of my problem.
I fixed the other piece of my code and now the authorization request succeeds without issue.

OctoKit with Auth0 (Github Login) in NextJS

I am building a Next JS app that has Github Login through Auth0 and uses the Octokit to fetch user info / repos.
In order to get the IDP I had to setup a management api in auth0. https://community.auth0.com/t/can-i-get-the-github-access-token/47237 which I have setup in my NodeJs server to hide the management api token as : GET /getaccesstoken endpoint
On the client side : /chooserepo page, I have the following code :
const chooserepo = (props) => {
const octokit = new Octokit({
auth: props.accessToken,
});
async function run() {
const res = await octokit.request("GET /user");
console.log("authenticated as ", res.data);
}
run();
And
export const getServerSideProps = withPageAuthRequired({
async getServerSideProps({ req, params }) {
let { user } = getSession(req);
console.log("user from get session ", user);
let url = "http://localhost:4000/getaccesstoken/" + user.sub;
let data = await fetch(url);
let resData = await data.text();
return {
props: { accessToken: resData }, // will be passed to the page component as props
};
},
});
However, I keep getting Bad credentials error. If I directly put the access token in the Octokit it seems to work well, but doesn't work when it's fetching the access token from the server.
It seems like Octokit instance is created before server side props are sent. How do I fix it ?
I figured out the error by comparing the difference between the request headers when hardcoding and fetching access token from server. Turns out quotes and backslashes need to be replaced (and aren't visible when just console logging)

OAuth with KeyCloak in Ktor : Is it supposed to work like this?

I tried to set up a working Oauth2 authorization via Keycloak in a Ktor web server. The expected flow would be sending a request from the web server to keycloak and logging in on the given UI, then Keycloak sends back a code that can be used to receive a token. Like here
First I did it based on the examples in Ktor's documentation. Oauth It worked fine until it got to the point where I had to receive the token, then it just gave me HTTP status 401. Even though the curl command works properly. Then I tried an example project I found on GitHub , I managed to make it work by building my own HTTP request and sending it to the Keycloak server to receive the token, but is it supposed to work like this?
I have multiple questions regarding this.
Is this function supposed to handle both authorization and getting the token?
authenticate(keycloakOAuth) {
get("/oauth") {
val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>()
call.respondText("Access Token = ${principal?.accessToken}")
}
}
I think my configuration is correct, since I can receive the authorization, just not the token.
const val KEYCLOAK_ADDRESS = "**"
val keycloakProvider = OAuthServerSettings.OAuth2ServerSettings(
name = "keycloak",
authorizeUrl = "$KEYCLOAK_ADDRESS/auth/realms/production/protocol/openid-connect/auth",
accessTokenUrl = "$KEYCLOAK_ADDRESS/auth/realms/production/protocol/openid-connect/token",
clientId = "**",
clientSecret = "**",
accessTokenRequiresBasicAuth = false,
requestMethod = HttpMethod.Post, // must POST to token endpoint
defaultScopes = listOf("roles")
)
const val keycloakOAuth = "keycloakOAuth"
install(Authentication) {
oauth(keycloakOAuth) {
client = HttpClient(Apache)
providerLookup = { keycloakProvider }
urlProvider = { "http://localhost:8080/token" }
}
}
There is this /token route I made with a built HTTP request, this one manages to get the token, but it feels like a hack.
get("/token"){
var grantType = "authorization_code"
val code = call.request.queryParameters["code"]
val requestBody = "grant_type=${grantType}&" +
"client_id=${keycloakProvider.clientId}&" +
"client_secret=${keycloakProvider.clientSecret}&" +
"code=${code.toString()}&" +
"redirect_uri=http://localhost:8080/token"
val tokenResponse = httpClient.post<HttpResponse>(keycloakProvider.accessTokenUrl) {
headers {
append("Content-Type","application/x-www-form-urlencoded")
}
body = requestBody
}
call.respondText("Access Token = ${tokenResponse.readText()}")
}
TL;DR: I can log in via Keycloak fine, but trying to get an access_token gives me 401. Is the authenticate function in ktor supposed to handle that too?
The answer to your first question: it will be used for both if that route corresponds to the redirect URI returned in urlProvider lambda.
The overall process is the following:
A user opens http://localhost:7777/login (any route under authenticate) in a browser
Ktor makes a redirect to authorizeUrl passing necessary parameters
The User logs in through Keycloak UI
Keycloak redirects the user to the redirect URI provided by urlProvider lambda passing parameters required for acquiring an access token
Ktor makes a request to the token URL and executes the routing handler that corresponds to the redirect URI (http://localhost:7777/callback in the example).
In the handler you have access to the OAuthAccessTokenResponse object that has properties for an access token, refresh token and any other parameters returned from Keycloak.
Here is the code for the working example:
val provider = OAuthServerSettings.OAuth2ServerSettings(
name = "keycloak",
authorizeUrl = "http://localhost:8080/auth/realms/master/protocol/openid-connect/auth",
accessTokenUrl = "http://localhost:8080/auth/realms/$realm/protocol/openid-connect/token",
clientId = clientId,
clientSecret = clientSecret,
requestMethod = HttpMethod.Post // The GET HTTP method is not supported for this provider
)
fun main() {
embeddedServer(Netty, port = 7777) {
install(Authentication) {
oauth("keycloak_oauth") {
client = HttpClient(Apache)
providerLookup = { provider }
// The URL should match "Valid Redirect URIs" pattern in Keycloak client settings
urlProvider = { "http://localhost:7777/callback" }
}
}
routing {
authenticate("keycloak_oauth") {
get("login") {
// The user will be redirected to authorizeUrl first
}
route("/callback") {
// This handler will be executed after making a request to a provider's token URL.
handle {
val principal = call.authentication.principal<OAuthAccessTokenResponse>()
if (principal != null) {
val response = principal as OAuthAccessTokenResponse.OAuth2
call.respondText { "Access token: ${response.accessToken}" }
} else {
call.respondText { "NO principal" }
}
}
}
}
}
}.start(wait = false)
}

Unable to Authorize users using Implicit / Authorization flow in google actions

I am trying to link to the account :
Here is my google cloud function
var AuthHandler = function() {
this.googleSignIn = googleSignIn;
this.googleSignInCallback = googleSignInCallback;
}
function googleSignIn(req, res, next) {
passport = req._passport.instance;
passport.authenticate('google',{scope: 'https://www.googleapis.com/auth/userinfo.email',
state:"google",response_type:"token"},
function(err, user, info) {
console.log(user);
})(req,res,next);
};
function googleSignInCallback(req, res, next) {
passport = req._passport.instance;
passport.authenticate('google',function(err, user, info) {
if(err) {
return next(err);
}
if(!user) {
return res.redirect('http://localhost:8000');
}
console.log(user._json.token);
// /res.redirect('/');
res.redirect('https://oauth-redirect.googleusercontent.com/r/xxxxxx#access_token=' + user._json.token + '&token_type=bearer&state=google')
})(req,res,next);
};
module.exports = AuthHandler;
In google Action Console :
I have created the implicit flow and gave my authorisation url as follows:
https://[region]-[projectid].cloudfunctions.net/[functionname]/auth/google
Error :
this is the browser Url
https://assistant.google.com/services/auth/handoffs/auth/complete?state=xxxx&code=xxxxxx
on which the following error is displayed
The parameter "state" must be set in the query string.
Update 1
Before starting this implementation , i have followed this Solution to create the Authentication.
Problems in this Approach :
1.As stated in the Documentation it is not redirecting to google.com and i'm unable to access the token using the APIAI SDK in javascript. but still i can see the Access token in emulator . for better understanding adding images
Here is my simulator O/P
{
"response": {
"debug": {
"agentToAssistantDebug": {
"assistantToAgentDebug": {
"assistantToAgentJson": "{"accessToken\":\"xxxxxx\""
}
},
"errors": []
}
Update 2 :
So i have started creating with implicit flow and here is my complete repo
After battling with it i have achieved it , as there is no proper articles about creation of own Oauth Server that implements the Google Action , this might helpful for future users.
Authorization Endpoint
app.get('/authorise', function(req, res) {
req.headers.Authorization = 'Bearer xxxxxxxxxxx';
// with your own mechanism after successful
//login you need to create a access token for the generation of
//authorization code and append it to this header;
var request = new Request(req);
var response = new Response(res);
oauth.authorize(request, response).then(function(success) {
// https://oauth-redirect.googleusercontent.com/r/YOUR_PROJECT_ID?
//code=AUTHORIZATION_CODE&state=STATE_STRING
var toredirect = success.redirectUri +"?code="+success.code
+"&state="+request.query.state ;
return res.redirect(toredirect);
}).catch(function(err){
res.status(err.code || 500).json(err)
}) });
Token Endpoint :
app.all('/oauth/token', function(req,res,next){
var request = new Request(req);
var response = new Response(res);
oauth
.token(request,response)
.then(function(token) {
// Todo: remove unnecessary values in response
return res.json(token)
}).catch(function(err){
return res.status(500).json(err)
})
});
After creation of this endpoints publish to the Google Cloud functions . I have used MYSQL as the DB using SEQUELIZE and Oauth-Server , if anyone need those models , will share it through repo .
With this you can able to link account using your own Server which implements
Auth tokens and Access Tokens
I think the problem is that the URL on this line isn't sending the parameters as query parameters, they're sending them as part of the anchor:
res.redirect('https://oauth-redirect.googleusercontent.com/r/xxxxxx#access_token=' + user._json.token + '&token_type=bearer&state=google')
You should replace the # with a ?, as illustrated here:
res.redirect('https://oauth-redirect.googleusercontent.com/r/xxxxxx?access_token=' + user._json.token + '&token_type=bearer&state=google')

adal.js inifnite loop when refreshing token

I am using the latest adal.js to query data from MicroSoft Dynamics CRM. The code gets into an infinite loop when renewing the token.
Additionally after loging into microsoft and being redirected back to my page the adaljs tries to refresh the token.
Note - this is javascript in an ASP.NET MVC web app. It is not using angular js.
This is also similar to the SO question Adal & Adal-Angular - refresh token infinite loop
var endpoints = {
orgUri: "https://<tenant>.crm6.dynamics.com/"
};
var config = {
clientId: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
tenant: '<tenant>.onmicrosoft.com',
redirectUri: 'http://localhost:53290/home/AuthenticatedByCrm/',
endpoints: endpoints,
cacheLocation: 'localStorage'
};
var x = new AuthenticationContext(config);
var isCallback = x.isCallback(window.location.hash);
if (isCallback) {
x.handleWindowCallback();
x.acquireToken(endpoints.orgUri, retrieveAccounts);
} else {
x.login();
}
function retrieveAccounts(error, token) {
// Handle ADAL Errors.
if (error || !token) {
alert('ADAL error occurred: ' + error);
return;
}
var req = new XMLHttpRequest();
req.open("GET", encodeURI(organizationURI + "/api/data/v8.0/accounts?$select=name,address1_city&$top=10"), true);
//Set Bearer token
req.setRequestHeader("Authorization", "Bearer " + token);
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.onreadystatechange = function () {
if (this.readyState == 4 /* complete */) {
req.onreadystatechange = null;
if (this.status == 200) {
var accounts = JSON.parse(this.response).value;
//renderAccounts(accounts);
}
else {
var error = JSON.parse(this.response).error;
console.log(error.message);
//errorMessage.textContent = error.message;
}
}
};
req.send();
}
The Active Directory Authentication Library (ADAL) for JavaScript helps you to use Azure AD for handling authentication in your single page applications. This library is optimized for working together with AngularJS.
Based on the investigation, this issue is caused by the handleWindowCallback. The response not able to run into the branch for if ((requestInfo.requestType === this.REQUEST_TYPE.RENEW_TOKEN) && window.parent && (window.parent !== window)) since it is not used in the Angular enviroment.
To integrate Azure AD with MVC application, I suggest that you using the Active Directory Authentication Library. And you can refer the code sample here.
Update
if (isCallback) {
// x.handleWindowCallback();
var requestInfo=x.getRequestInfo(window.location.hash);
//get the token provided resource. to get the id_token, we need to pass the client id
var token = x.getCachedToken("{clientId}")
x.saveTokenFromHash(requestInfo);
} else {
x.login();
}