Nylas supports Hosted OAuth to get the user’s authorization for scopes and create their grant, and PKCE for extra security during the process.
If you’re developing a single page application (SPA) or a mobile app, we recommend you use [Hosted OAuth with PKCE]. This extra layer of security adds a key to the authentication exchange that you can safely store on a mobile device, instead of including the API key. This is optional for projects that have a backend, but it’s a good security practice to implement anyway.
How Hosted OAuth works
Nylas creates only one grant per email address in each application. If a user authenticates with your Nylas application using the email address associated with an existing grant, Nylas re-authenticates the grant instead of creating a new one.
- The user clicks a link or button in your project to start an authorization request.
- Nylas forwards the user to their provider where they complete the authorization flow.
- The provider directs the user to the Nylas callback URI and includes URL parameters that indicate whether the authorization succeeded or failed, along with other information.
- If the authorization succeeded, Nylas creates an unverified grant record.
- Nylas forwards the user to your project’s callback URI and includes the access
code
from the provider as a URL parameter. - Your project uses the
code
to perform a token exchange with the provider. - When the token exchange completes successfully, Nylas marks the grant record as verified and creates a grant ID for the user.
Start an authorization request
The first step of the authentication process is to start an authorization request. Usually this is a button or link that the user clicks.
Your project redirects the user to the authorization request endpoint and includes their information as a set of query parameters, as in the example below. When the user goes to this URL, Nylas starts a secure authentication session and redirects them to their provider’s website.
Each provider displays their authorization consent and approval steps differently. The steps are visible only to the user.
If a user authenticates using their Google account, they might be directed to Google’s authorization page twice. This is a normal part of the Hosted OAuth flow, and it ensures that the user approves all necessary scopes.
Use access_type
to request refresh tokens
You can use the access_type
parameter in your authorization request to indicate whether you want Nylas to return a refresh token for the grant.
If you’re developing a mobile or client-side-only app, we recommend you use access_type=online
. This prevents the OAuth process from creating a refresh token. When you use this method, your users need to re-authenticate manually when their access token expires.
Otherwise, you can use access_type=offline
to get a refresh token when a user authenticates. You can use the refresh token to request a new access token for the user when their old one expires, without prompting them to re-authenticate.
For security reasons, we strongly recommend against using access_type=offline
for mobile and client-side-only apps. In these cases it’s best for your users to manually re-authenticate when their access tokens expire.
Pass user information in state
parameter
Nylas Hosted OAuth supports the optional state
parameter. If you include it in an authorization request, Nylas returns the unmodified value to your project. You can use this as a verification check, or to track information about the user that you need when creating a grant or logging them in.
For more information about the state
parameter, see the OAuth 2.0 specification and the official OAuth 2.0 documentation.
Accept authorization response
After the user completes the authorization process, their provider sends them to Nylas’ redirect URI (https://api.us.nylas.com/v3/connect/callback
) and includes URL parameters with information about the user. Nylas uses the information in the parameters to find your application using its client ID and, if the authentication succeeded, create an unverified grant record for the user.
Nylas then uses your application’s callback URI to direct the user back to your project, along with the code
it received from the provider.
https://myapp.com/callback-handler?code=<CODE>
If you specified a state
in the initial authorization request, Nylas includes it as a URL parameter.
Exchange code
for access token
The OAuth code
is a unique, one-time-use credential. This means that if your POST /v3/connect/token
request fails, you’ll need to restart the OAuth flow to generate a new code
. If you try to pass the original code
in another token exchange request, Nylas returns an error.
Make a POST /v3/connect/token
request to exchange the user’s code
for an access token. Nylas returns an access token and other information about the user.
POST /token HTTP/1.1Host: /v3/connect/tokenContent-Type: application/json
{ "client_id": "<NYLAS_CLIENT_ID>", "client_secret": "<NYLAS_API_KEY>", "grant_type": "authorization_code" "code": "<CODE>", "redirect_uri": "<CALLBACK_URI>",}
app.get('/oauth/exchange', async (req, res) => { console.log(res.status)
const code = req.query.code
if (!code) { res.status(400).send('No authorization code returned from Nylas') return }
try { const codeExchangeResponse = nylas.auth.exchangeCodeForToken({ redirectUri: "REDIRECT_URI", clientId: "CLIENT_ID", clientSecret: "API_KEY", code: "CODE" });
const { grantId } = response
res.status(200) } catch (error) { console.error('Error exchanging code for token:', error)
res.status(500).send('Failed to exchange authorization code for token') }})
from dotenv import load_dotenvload_dotenv()
import jsonimport osfrom functools import wrapsfrom io import BytesIOfrom flask import Flaskfrom nylas import Client
nylas = Client( os.environ.get("NYLAS_CLIENT_ID"), os.environ.get("NYLAS_API_URI"))
REDIRECT_CLIENT_URI = 'http://localhost:9000/oauth/exchange'
@flask_app.route("/oauth/exchange", methods=["GET"])
def exchange_code_for_token(): code_exchange_response = nylas.auth.exchange_code_for_token( request={ "code": request.args.get('code'), "client_id": os.environ.get("NYLAS_CLIENT_ID"), "redirect_uri": REDIRECT_CLIENT_URI } )
return { 'email_address': code_exchange_response.email, 'grant_id': code_exchange_response.grant_id }
get '/oauth/exchange' do code = params[:code] status 404 if code.nil?
begin response = nylas.auth.exchange_code_for_token({ client_id: "<NYLAS_CLIENT_ID>", redirect_uri: 'http://localhost:4567/oauth/exchange', code: code }) rescue StandardError status 500 else grant_id = response[:grant_id] email = response[:email]
"Grant_Id: #{grant_id} \n Email: #{email}" endend
http.get("/oauth/exchange") { val code : String = request.queryParams("code")
if(code == "") { response.status(401) }
val codeRequest : CodeExchangeRequest = CodeExchangeRequest( "http://localhost:4567/oauth/exchange", code, "<NYLAS_CLIENT_ID>", "nylas" )
try { val codeResponse : CodeExchangeResponse = nylas.auth().exchangeCodeForToken(codeRequest)
codeResponse } catch (e : Exception) { e }}
get("/oauth/exchange", (request, response) -> { String code = request.queryParams("code");
if(code == null) { response.status(401); }
assert code != null;
CodeExchangeRequest codeRequest = new CodeExchangeRequest( "http://localhost:4567/oauth/exchange", code, "<NYLAS_CLIENT_ID>", "nylas" );
try { CodeExchangeResponse codeResponse = nylas.auth().exchangeCodeForToken(codeRequest);
return "%s".formatted(codeResponse); } catch(Exception e) { return "%s".formatted(e); }});
The user’s provider responds with an access token, a refresh token (because you set access_type=offline
in your authorization request), and some other information.
{ "access_token": "<ACCESS_TOKEN>", "refresh_token": "<REFRESH_TOKEN>", "scope": "<SCOPES>", "token_type": "Bearer", "id_token": "<ID_TOKEN>", "grant_id": "<NYLAS_GRANT_ID>"}
OAuth 2.0 access tokens expire after one hour. When the access token expires, you can either use the refresh token to get a new access token, or the user can re-authenticate their grant to access your project.
Nylas marks the user’s grant as verified and sends you their grant ID and email address.
Your project should store the user’s grant ID, access token, and refresh token (for later re-authentication).
Secure the authentication process with PKCE
The OAuth PKCE (Proof Key for Code Exchange) flow improves security for client-side-only applications, such as browser-based or mobile apps that don’t have a backend server. Even if your project does have a backend server, we recommend you use PKCE for extra security.
Never store application-wide credentials like API keys in mobile or client-side code. You should complete the code
exchange flow without using your Nylas application’s API key. If you’re using PKCE, you can set platform
to android
, desktop
, ios
, or js
when you create a callback URI to make the client_secret
field optional.
Create a code_challenge
Before you make an authentication request using PKCE, you need to create a code_challenge
. You’ll use this when you [make an authorization request].
The following example uses nylas
as a code verification string and sets the encoding method to S256
for extra security.
If you don’t set an encoding method, Nylas assumes you’re using a plain text code verification string. We strongly recommend you use SHA-256 encoding to create a more secure code_challenge
.
- Hash the verification string using an SHA-256 encoding tool (
SHA256("nylas")
->e96bf6686a3c3510e9e927db7069cb1cba9b99b022f49483a6ce3270809e68a2
). - Convert the hashed string to Base64 encoding and remove any padding (
e96bf6686a3c3510e9e927db7069cb1cba9b99b022f49483a6ce3270809e68a2
->ZTk2YmY2Njg2YTNjMzUxMGU5ZTkyN2RiNzA2OWNiMWNiYTliOTliMDIyZjQ5NDgzYTZjZTMyNzA4MDllNjhhMg
). - Save the resulting encoded string to use as the
code_challenge
in your [authorization request].
Make authorization request with code_challenge
Make a GET /v3/connect/auth
request to create a URL that redirects your user to the authorization flow.
/connect/auth? client_id=<NYLAS_CLIENT_ID> &redirect_uri=https://myapp.com/callback-handler &response_type=code &provider=<PROVIDEr> &scope=<SCOPES> &state=<STATE> &code_challenge=ZTk2YmY2Njg2YTNjMzUxMGU5ZTkyN2RiNzA2OWNiMWNiYTliOTliMDIyZjQ5NDgzYTZjZTMyNzA4MDllNjhhMg &code_challenge_method=S256
import 'dotenv/config'import express from 'express'import Nylas from 'nylas'
// Nylas configurationconst config = { clientId: process.env.NYLAS_CLIENT_ID, redirectUri: "http://localhost:3000/oauth/exchange", apiKey: process.env.NYLAS_API_KEY, apiUri: process.env.SERVER_URL,}
const config = { apiKey: config.apiKey, apiUri: config.apiUri,}
const nylas = new Nylas(config)
const app = express()const port = 3000
// Route to start the OAuth flowapp.get('/nylas/auth', (req, res) => { const authData = nylas.auth.urlForOAuth2PKCE({ clientId: config.clientId, provider: 'google', redirectUri: config.redirectUri, loginHint: 'enter-email-address-here', })
res.redirect(authData.url)})
from dotenv import load_dotenvload_dotenv()
import jsonimport osfrom functools import wraps
from io import BytesIOfrom flask import Flask, redirectfrom flask_cors import CORS
from nylas import Client
nylas = Client( os.environ.get("NYLAS_CLIENT_ID"), os.environ.get("NYLAS_API_URI"))
REDIRECT_CLIENT_URI = 'http://localhost:9000/oauth/exchange'
flask_app = Flask(__name__)
@flask_app.route("/nylas/generate-auth-url", methods=["GET"])def build_auth_url(): auth_url = nylas.auth.url_for_oauth2_pkce( config={ "client_id": os.environ.get("NYLAS_CLIENT_ID"), "provider": 'google', "redirect_uri": REDIRECT_CLIENT_URI, "login_hint": 'enter-email-address-here' } )
return redirect(auth_url)
# frozen_string_literal: true
require 'nylas'require 'sinatra'
set :show_exceptions, :after_handler
error 404 do 'No authorization code returned from Nylas'end
error 500 do 'Failed to exchange authorization code for token'end
nylas = Nylas::Client.new( api_key: "<NYLAS_API_KEY>")
get '/nylas/auth' do config = { client_id: "<NYLAS_CLIENT_ID>", provider: 'google', redirect_uri: 'http://localhost:4567/oauth/exchange', login_hint: '<email_to_connect>', }
authData = nylas.auth.url_for_oauth2_pkce(config) redirect authData.urlend
import com.nylas.NylasClientimport com.nylas.models.*import spark.kotlin.Httpimport spark.kotlin.ignite
fun main(args: Array<String>) { val nylas: NylasClient = NylasClient( apiKey = "<NYLAS_API_KEY>" )
val http: Http = ignite()
http.get("/nylas/auth") { val scope = listOf("https://www.googleapis.com/auth/userinfo.email")
val config : UrlForAuthenticationConfig = UrlForAuthenticationConfig( "<GCP_CLIENT_ID>", "<GCP_CLIENT_SECRET>", AccessType.ONLINE, AuthProvider.GOOGLE, Prompt.DETECT, scope, true, "sQ6vFQN", "<email_to_connect>" )
var authData = nylas.auth().urlForOAuth2PKCE(config)
response.redirect(authData.url) }}
import java.util.*;import static spark.Spark.*;import com.nylas.NylasClient;import com.nylas.models.*;
public class AuthRequest { public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError { NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
get("/nylas/auth", (request, response) -> { List<String> scope = new ArrayList<>(); scope.add("https://www.googleapis.com/auth/userinfo.email");
UrlForAuthenticationConfig config = new UrlForAuthenticationConfig("<GCP_CLIENT_ID>", "http://localhost:4567/oauth/exchange", AccessType.ONLINE, AuthProvider.GOOGLE, Prompt.DETECT, scope, true, "sQ6vFQN",
PKCEAuthURL authData = nylas.auth().urlForOAuth2PKCE(config);
response.redirect(authData.getUrl());
return null; }); }}
Exchange code
for access token with code_challenge
The rest of the OAuth flow proceeds as usual: your project redirects the user to their provider where they authenticate and either accept or reject the requested scopes. The provider then sends them back to Nylas with an authorization code
, and Nylas creates an unverified grant record. Nylas returns the user to your project with the code
.
Next, use the code
in a POST /v3/connect/token
request to get an access token. Because you’re using PKCE, you need to set the grant_type
to authorization_code
and include your code_verifier
.
For readability, the example below sets code_verifier
to the original plain text code_challenge
value.
POST /token HTTP/1.1Host: /v3/connect/tokenContent-Type: application/json
{ "client_id": "<NYLAS_CLIENT_ID>", "redirect_uri": "<REDIRECT_URI>", "grant_type": "authorization_code", "code": "<AUTH_EXCHANGE_CODE>", "code_verifier": "nylas"}
app.get('/oauth/exchange', async (req, res) => { console.log(res.status)
const code = req.query.code
if (!code) { res.status(400).send('No authorization code returned from Nylas') return }
try { const codeExchangeResponse = nylas.auth.exchangeCodeForToken({ redirectUri: "REDIRECT_URI", clientId: "CLIENT_ID", clientSecret: "API_KEY", code: "CODE" });
const { grantId } = response
res.status(200) } catch (error) { console.error('Error exchanging code for token:', error)
res.status(500).send('Failed to exchange authorization code for token') }})
from dotenv import load_dotenvload_dotenv()
import jsonimport osfrom functools import wrapsfrom io import BytesIOfrom flask import Flaskfrom nylas import Client
nylas = Client( os.environ.get("NYLAS_CLIENT_ID"), os.environ.get("NYLAS_API_URI"))
REDIRECT_CLIENT_URI = 'http://localhost:9000/oauth/exchange'
@flask_app.route("/oauth/exchange", methods=["GET"])
def exchange_code_for_token(): code_exchange_response = nylas.auth.exchange_code_for_token( request={ "code": request.args.get('code'), "client_id": os.environ.get("NYLAS_CLIENT_ID"), "redirect_uri": REDIRECT_CLIENT_URI } )
return { 'email_address': code_exchange_response.email, 'grant_id': code_exchange_response.grant_id }
set :show_exceptions, :after_handler
error 404 do 'No authorization code returned from Nylas'end
error 500 do 'Failed to exchange authorization code for token'end
get '/oauth/exchange' do code = params[:code] status 404 if code.nil?
begin response = nylas.auth.exchange_code_for_token({ client_id: "<NYLAS_CLIENT_ID>", redirect_uri: 'http://localhost:4567/oauth/exchange', code_verifier: 'insert-code-challenge-secret-hash', code: code }) rescue StandardError status 500 else grant_id = response[:grant_id] email = response[:email]
"Grant_Id: #{grant_id} \n Email: #{email}" endend
http.get("/oauth/exchange") { val code : String = request.queryParams("code")
if(code == "") { response.status(401) }
val codeRequest : CodeExchangeRequest = CodeExchangeRequest( "http://localhost:4567/oauth/exchange", code, "<NYLAS_CLIENT_ID>", "<CODE_CHALLENGE>" )
try { val codeResponse : CodeExchangeResponse = nylas.auth().exchangeCodeForToken(codeRequest)
codeResponse } catch (e : Exception) { e }}
get("/oauth/exchange", (request, response) -> { String code = request.queryParams("code");
if(code == null) { response.status(401); }
assert code != null;
CodeExchangeRequest codeRequest = new CodeExchangeRequest( "http://localhost:4567/oauth/exchange", code, "<NYLAS_CLIENT_ID>", "<CODE_CHALLENGE>" );
try { CodeExchangeResponse codeResponse = nylas.auth().exchangeCodeForToken(codeRequest);
return "%s".formatted(codeResponse); } catch(Exception e) { return "%s".formatted(e); }});
Make API requests with access token
Now that you have a grant for your user, you can make requests on their behalf with their access token and the /me/
syntax.
You can’t use an access token to authorize API requests that access or modify data at the application level. Those requests require an API key for authorization.
To authorize an API request, pass the user’s access token in the request header and substitute me
where you’d usually specify a grant ID.
curl --request GET \ --url 'https://api.us.nylas.com/v3/grants/me/calendars' \ --header 'Authorization: Bearer <ACCESS_TOKEN>' \ --header 'Content-Type: application/json'
When Nylas receives a request using the /me/
syntax, it checks the authorization header token, finds the associated grant ID, and uses that ID to locate the user’s data.
Refresh an expired access token
If you set access_type=offline
in your authorization request, Nylas returns a refresh token along with the access token during the token exchange process. When the access token expires, you can use the refresh token to request a new one.
Refresh tokens don’t expire unless they’re revoked. If your project is client-side-only, you shouldn’t request offline access or need this step.
Make a POST /v3/connect/token
request that sets the grant_type
to refresh_token
and includes the user’s refresh token. The user’s provider returns a fresh access token for their grant.
POST /token HTTP/1.1Host: /v3/connect/tokenContent-Type: application/json
{ "client_id": "<NYLAS_CLIENT_ID>", "client_secret": "<NYLAS_API_KEY>", "grant_type": "refresh_token", "refresh_token": "<REFRESH_TOKEN>"}
{ "access_token": "<ACCESS_TOKEN>", "scope": "https://www.googleapis.com/auth/gmail.readonly profile", "token_type": "Bearer"}
const config = { clientId: process.env.NYLAS_CLIENT_ID, callbackUri: "http://localhost:3000/oauth/exchange", apiKey: process.env.NYLAS_API_KEY, apiUri: process.env.NYLAS_API_URI}
const nylas = new Nylas({ apiKey: config.apiKey, apiUri: config.apiUri})
const refreshed = await nylas.auth.refreshAccessToken({ clientId: config.clientId, refreshToken: response.refreshToken, redirectUri: config.redirectUri})
from dotenv import load_dotenvload_dotenv()
import osimport sysfrom nylas import Client
nylas = Client( os.environ.get('NYLAS_API_KEY'), os.environ.get('NYLAS_API_URI'))
REDIRECT_CLIENT_URI = 'http://localhost:9000/oauth/exchange'
response = nylas.auth.refresh_access_token( request={ "client_id": os.environ.get("NYLAS_CLIENT_ID"), "refresh_token": '<NYLAS_REFRESH_TOKEN>', "redirect_uri": REDIRECT_CLIENT_URI })
get '/nylas/refresh' do request = { clientId: "<NYLAS_CLIENT_ID>", refreshToken: "<NYLAS_REFRESH_TOKEN>", redirectUri: "http://localhost:4567/oauth/exchange" }
refreshed_token = nylas.auth.refresh_access_token(request)end
http.get("/nylas/refresh") { val token = TokenExchangeRequest( "http://localhost:4567/oauth/exchange", "<NYLAS_REFRESH_TOKEN>", "<NYLAS_API_KEY>" )
nylas.auth().refreshAccessToken(token)}
get("/nylas/refresh", (request, response) -> { TokenExchangeRequest token = new TokenExchangeRequest( "http://localhost:4567/oauth/exchange", "<NYLAS_REFRESH_TOKEN>", "<NYLAS_CLIENT_ID>" );
return nylas.auth().refreshAccessToken(token);});
Handle authentication errors
If the authentication process fails, Nylas returns the standard OAuth 2.0 error fields in its response: error
, error_description
, and error_uri
.
https://myapp.com/callback-handler? state=... # Passed value of initial state if it was provided. &error=... # Error type/constant. &error_description=... # Error description. &error_uri=... # Error or event code.
If an unexpected error occurs during the callback URI creation step at the end of the authentication flow, Nylas’ response includes the error_code
field instead of error_uri
.
https://myapp.com/callback-handler? &error=internal_error # Error type/constant. &error_description=Internal+error%2C+contact+administrator # Error description. &error_code=500 # Code of internal error.