Stencil.js: Unexpected URL behaviour - URL auto reloads to lowercase after a few milliseconds - browser

I cannot understand why when my stencil.js app loads, it runs the componentWillLoad() lifecycle method twice, and just before the second one forces the url to lowercase. This is a problem as the url contains an encoded UID so when it is forced to lowercase and then I extract the encoded uid, I can no longer decode in the backend.
The full story:
I am working on an account activation feature.
My django backend uses Djoser, Sendgrid and Anymail to send an email activation link to a user after their account is created. The uid of the user is encoded using djoser.utils.encode_uid(uid) and added to the url
The user receives the email and clicks a button which takes them to a page on my stencil app where they (as an anonymous user) enter their password and name.
When the form is submitted, the token from the url is used by the backend to authenticate the activation of the user's account. The user to be activated is determined by the uid from the url once the backend has decoded it using `djoser.utils.decode_uid(uid).
The problem:
After opening the stencil page from the email link (baseUrl/users/{uid}/activate/{token}/, for a few milliseconds, the URL is perfect for my needs as the encoded uid is exactly as it was encoded by my django app (mixed case). At this point, my componentWillLoad() lifecycle method extracts the encoded uid and the token from the url and stores it in the component state. However, after literally a few milliseconds, the page reloads and the url has the same characters but is now lowercase. This is an issue as the encoded UID has now changed (djoser encoding is case sensitive). The componentWillLoad() runs again, extracts the now incorrect encoded UID, and submits it in the form and so the django backend fails to decode it and identify the user to activate.
Here is a partial of my stencil component:
#Component({
tag: 'app-activate',
styleUrl: 'app-activate.css',
})
export class AppActivate {
#State() name: string
#State() new_password: string
#State() re_new_password: string
#State() email: string
#State() uid: string
#State() token: string
api: Auricle
constructor() {
this.api = new Auricle()
}
getDataFromUrl(newUrl: string) {
console.log('fetching values')
const splitOne = newUrl.split('users/')[1]
const uid = splitOne.split('activate/')[0].slice(0, -1)
const token = splitOne.split('activate/')[1]
console.log('SPLIT RESULT: ', { uid: uid, token: token, orginal_url: window.location.href })
const encodedUid = uid
const decodedUid = atob(encodedUid)
console.log('test', decodedUid) // Output: "1234567890"
return { uid, token }
}
componentWillLoad() {
const values = this.getDataFromUrl(Router.url.pathname)
this.uid = values['uid']
this.token = values.token
}
render() {
return (
///
)
}
}
///
Here is the package.json dependencies for my stencil app:
"devDependencies": {
"#fullhuman/postcss-purgecss": "^4.0.3",
"#reduxjs/toolkit": "1.9.1",
"#rollup/plugin-node-resolve": "^13.3.0",
"#stencil/core": "npm:#johnjenkins/stencil-community#2.4.0",
"#stencil/postcss": "^2.0.0",
"#stencil/router": "^1.0.1",
"#types/css-font-loading-module": "0.0.4",
"#types/node": "^14.14.41",
"autoprefixer": "^10.2.5",
"cross-env": "^7.0.3",
"cssnano": "^5.0.1",
"postcss-import": "^14.0.2",
"postcss-replace": "^1.1.3",
"release-it": "*",
"rollup": "^2.50.5",
"rollup-plugin-dotenv": "^0.4.0",
"rollup-plugin-node-polyfills": "^0.2.1",
"rollup-plugin-node-resolve": "^5.2.0",
"tailwindcss": "^2.1.1",
"typescript": "^2.9.2"
},
"license": "MIT",
"dependencies": {
"#codexteam/shortcuts": "^1.1.1",
"#editorjs/editorjs": "^2.22.2",
"#ionic/core": "^5.6.6",
"#ionic/storage": "^3.0.4",
"#types/jest": "^26.0.22",
"custom-avatar-initials": "^1.0.1",
"fuse.js": "^6.4.6",
"jwt-check-expiration": "^1.0.5",
"localforage-driver-memory": "^1.0.5",
"lodash.sortby": "^4.7.0",
"lodash.truncate": "^4.4.2",
"moment": "^2.29.1",
"stencil-router-v2": "^0.6.0"
}
To reiterate, the flow I need is:
User creates account
User receives activation email
User follows link (which includes uid & token)
URL path variables are extracted without the url reloading in lowercase.
I know that the encoding is correct as for a split second it appears correctly in the browser. If I log it the first time componentWillLoad() runs, and then copy the encoded UID and decode it, it gives me the correct UID. So the issue is definitely that for some reason, sometime after the first componentWillLoad() (which should only run once) the url is forced to lowercase and the lifecycle methods start again.
Ideally, I would like to ensure that the url does not change at all, but failing that, can anyone think of any workarounds to store the encoded UID from the url on the first componentWillLoad() run, and then use that initial value in the form submission?
thanks in advance
UPDATE
I have come up with one workaround, which I will share in case it is useful for others however I must disclaim that I dislike it for two reasons:
Primarily, I find the initial behaviour of the url formatting to lowercase inexplicable and feel frustrated that I have not found any documentation that explain why this happens or how to prevent it. I assume the stencil-router-v2 is responsible for the behaviour and probably has some built-in normalising hook for urls (perhaps for SEO reasons). I would be very grateful for any more informed explanation.
My solution uses localstorage, which while effective, feels more hacky than necessary for the task at hand. There may even be some security risks to this, given that the uid and activation token are being stored.
My workaround:
correctUrlWorkAround(uid: string, token: string) {
const storedUID = localStorage.getItem('activation_uid')
const storedToken = localStorage.getItem('activation_token')
if (storedUID) {
this.activationData['uid'] = storedUID
} else {
// Store the value in localStorage
localStorage.setItem('activation_uid', uid)
this.activationData['uid'] = localStorage.getItem('activation_uid')
}
if (storedToken) {
this.activationData['token'] = storedToken
} else {
// Store the value in localStorage
localStorage.setItem('activation_token', token)
this.activationData['token'] = localStorage.getItem('activation_token')
}
}

Related

How to check if two URL's lead to the same path?

I'm building a URL Shortener, and I've decided to recyle the short ID's if possible to save space in my database. How can i check if 2 URL's lead to the same path?
For example, let's say a user generates a short URL for https://google.com/.
My app generates the following short id: jkU3
So if this user visits https://tiny.url/jkU3 my express server will redirect the visitor to https://google.com/.
This works like a charm, but know let's imagine another person visits https://tiny.url/ and generates a short URL for https://google.com. And another one comes and generates a short URL for https://www.google.com/, and another one comes and generates one for https://www.google.com. You get the point..
So far my app would have wasted 4 short ID's.
How can i prevent this from happening? Is there a regex for this?
This is the current code I have for generating short URL's:
app.post("/", (req: Request, res: Response) => {
const shortUrl: string = nanoid(4);
const destination: string = req.body.destination;
UrlSchema.create({
_id: mongoose.Types.ObjectId(),
origin: shortUrl,
destination: destination,
}).then(() => {
// Unique Id
res.json(shortUrl);
});
});
Before creating a new entry you can check work destination with
const existing = await UrlSchema.findOne({destination:req.body.destination});
if(!existing){
// create new
} else{
// return same
}
This way you will be creating destination if it does not exist already. You can remove tariling slash(/) if it exists to match URLs better,
You've listed four slightly different URLs:
https://www.google.com
https://google.com
https://www.google.com/
https://google.com/
None of these are technically the same https request, though it sounds like you want to assume that the / at the end is optional and thus does not make it a different target URL.
The last two are not guaranteed to be the same host as the first two. For google.com and www.google.com, they are the same host, but this is not guaranteed to be the case for all possible hosts.
If you want to assume that these four are all to the same host no matter what the domain is, then you just have to normalize the URL before you put it in your database and then before assigning a new shortened ID, you search the database for the normalized version of the URL.
In this case, you would remove the www. and remove any trailing slash to create the normalized version of the URL.
function normalizeUrl(url) {
// remove "www." if at first part of hostname
// remove trailing slash
return url.replace(/\/\/www\./, "//").replace(/\/$/, "");
}
Once you've normalized the URL, you search for the normalized URL in your database. If you find it, you use the existing shortener for it. If you don't find it, you add the normalized version to your database with a newly generated shortId.
Here's a demo:
function normalizeUrl(url) {
// remove "www." if at first part of hostname
// remove trailing slash
return url.replace(/\/\/www\./i, "//").replace(/\/$/, "");
}
const testUrls = [
"https://www.google.com",
"https://www.google.com/",
"https://google.com",
"https://google.com/",
];
for (const url of testUrls) {
console.log(normalizeUrl(url));
}
FYI, since hostnames in DNS are not case sensitive, you may also want to force the hostname to lower case to normalize it. Path names or query parameters could be case sensitive (sometimes they are and sometime they are not).
To include the host case sensitivity normalization, you could use this:
function normalizeUrl(url) {
// remove "www." if at first part of hostname
// remove trailing slash
// lowercase host name
return newUrl = url.replace(/\/\/www\./i, "//").replace(/\/$/, "").replace(/\/\/([^/]+)/, function(match, p1) {
// console.log(match, p1);
return "//" + p1.toLowerCase();
});
}
const testUrls = [
"https://www.google.com",
"https://www.google.com/",
"https://google.com",
"https://google.com/",
"https://WWW.google.com",
"https://www.Google.com/",
"https://GOOGLE.com",
"https://google.COM/",
"https://www.Google.com/xxx", // this should be unique
"https://google.COM/XXX", // this should be unique
];
for (const url of testUrls) {
console.log(normalizeUrl(url));
}

Using Cookies in JSF Application to remember login credentials [duplicate]

On most websites, when the user is about to provide the username and password to log into the system, there's a checkbox like "Stay logged in". If you check the box, it will keep you logged in across all sessions from the same web browser. How can I implement the same in Java EE?
I'm using FORM based container managed authentication with a JSF login page.
<security-constraint>
<display-name>Student</display-name>
<web-resource-collection>
<web-resource-name>CentralFeed</web-resource-name>
<description/>
<url-pattern>/CentralFeed.jsf</url-pattern>
</web-resource-collection>
<auth-constraint>
<description/>
<role-name>STUDENT</role-name>
<role-name>ADMINISTRATOR</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>FORM</auth-method>
<realm-name>jdbc-realm-scholar</realm-name>
<form-login-config>
<form-login-page>/index.jsf</form-login-page>
<form-error-page>/LoginError.jsf</form-error-page>
</form-login-config>
</login-config>
<security-role>
<description>Admin who has ultimate power over everything</description>
<role-name>ADMINISTRATOR</role-name>
</security-role>
<security-role>
<description>Participants of the social networking Bridgeye.com</description>
<role-name>STUDENT</role-name>
</security-role>
Java EE 8 and up
If you're on Java EE 8 or newer, put #RememberMe on a custom HttpAuthenticationMechanism along with a RememberMeIdentityStore.
#ApplicationScoped
#AutoApplySession
#RememberMe
public class CustomAuthenticationMechanism implements HttpAuthenticationMechanism {
#Inject
private IdentityStore identityStore;
#Override
public AuthenticationStatus validateRequest(HttpServletRequest request, HttpServletResponse response, HttpMessageContext context) {
Credential credential = context.getAuthParameters().getCredential();
if (credential != null) {
return context.notifyContainerAboutLogin(identityStore.validate(credential));
}
else {
return context.doNothing();
}
}
}
public class CustomIdentityStore implements RememberMeIdentityStore {
#Inject
private UserService userService; // This is your own EJB.
#Inject
private LoginTokenService loginTokenService; // This is your own EJB.
#Override
public CredentialValidationResult validate(RememberMeCredential credential) {
Optional<User> user = userService.findByLoginToken(credential.getToken());
if (user.isPresent()) {
return new CredentialValidationResult(new CallerPrincipal(user.getEmail()));
}
else {
return CredentialValidationResult.INVALID_RESULT;
}
}
#Override
public String generateLoginToken(CallerPrincipal callerPrincipal, Set<String> groups) {
return loginTokenService.generateLoginToken(callerPrincipal.getName());
}
#Override
public void removeLoginToken(String token) {
loginTokenService.removeLoginToken(token);
}
}
You can find a real world example in the Java EE Kickoff Application.
Java EE 6/7
If you're on Java EE 6 or 7, homegrow a long-living cookie to track the unique client and use the Servlet 3.0 API provided programmatic login HttpServletRequest#login() when the user is not logged-in but the cookie is present.
This is the easiest to achieve if you create another DB table with a java.util.UUID value as PK and the ID of the user in question as FK.
Assume the following login form:
<form action="login" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<input type="checkbox" name="remember" value="true" />
<input type="submit" />
</form>
And the following in doPost() method of a Servlet which is mapped on /login:
String username = request.getParameter("username");
String password = hash(request.getParameter("password"));
boolean remember = "true".equals(request.getParameter("remember"));
User user = userService.find(username, password);
if (user != null) {
request.login(user.getUsername(), user.getPassword()); // Password should already be the hashed variant.
request.getSession().setAttribute("user", user);
if (remember) {
String uuid = UUID.randomUUID().toString();
rememberMeService.save(uuid, user);
addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE);
} else {
rememberMeService.delete(user);
removeCookie(response, COOKIE_NAME);
}
}
(the COOKIE_NAME should be the unique cookie name, e.g. "remember" and the COOKIE_AGE should be the age in seconds, e.g. 2592000 for 30 days)
Here's how the doFilter() method of a Filter which is mapped on restricted pages could look like:
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
User user = request.getSession().getAttribute("user");
if (user == null) {
String uuid = getCookieValue(request, COOKIE_NAME);
if (uuid != null) {
user = rememberMeService.find(uuid);
if (user != null) {
request.login(user.getUsername(), user.getPassword());
request.getSession().setAttribute("user", user); // Login.
addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE); // Extends age.
} else {
removeCookie(response, COOKIE_NAME);
}
}
}
if (user == null) {
response.sendRedirect("login");
} else {
chain.doFilter(req, res);
}
In combination with those cookie helper methods (too bad they are missing in Servlet API):
public static String getCookieValue(HttpServletRequest request, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setPath("/");
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}
public static void removeCookie(HttpServletResponse response, String name) {
addCookie(response, name, null, 0);
}
Although the UUID is extremely hard to brute-force, you could provide the user an option to lock the "remember" option to user's IP address (request.getRemoteAddr()) and store/compare it in the database as well. This makes it a tad more robust. Also, having an "expiration date" stored in the database would be useful.
It's also a good practice to replace the UUID value whenever the user has changed its password.
Java EE 5 or below
Please, upgrade.
Normally this is done like this:
When you log in a user you also set a cookie on the client ( and store the cookie value in the database ) expiring after a certain time (1-2 weeks usually).
When a new request comes in you check that the certain cookie exists and if so look into the database to see if it matches a certain account. If it matches you will then "loosely" log in that account. When i say loosely i mean you only let that session read some info and not write information. You will need to request the password in order to allow the write options.
This is all that is. The trick is to make sure that a "loosely" login is not able to do a lot of harm to the client. This will somewhat protect the user from someone who grabs his remember me cookie and tries to log in as him.
You cannot login a user completely via HttpServletRequest.login(username, password) since you shouldn't keep both username and plain text password in the database. Also you cannot perform this login with a password hash which is saved in the database. However, you need to identify a user with a cookie/DB token but log him/her in without entering password using custom login module (Java class) based on Glassfish server API.
See the following links for more details:
http://www.lucubratory.eu/custom-jaas-realm-for-glassfish-3/
Custom Security mechanism in Java EE 6/7 application
Although the answer by BalusC (the part for Java EE 6/7) gives useful hints, I doesn't work in modern containers, because you can't map a login filter to pages that are protected in a standard way (as confirmed in the comments).
If for some reason you can't use Spring Security (which re-implements the Servlet Security in an incompatible way), then it's better to stay with <auth-method>FORM and put all the logic into an active login page.
Here's the code (the full project is here: https://github.com/basinilya/rememberme )
web.xml:
<form-login-config>
<form-login-page>/login.jsp</form-login-page>
<form-error-page>/login.jsp?error=1</form-error-page>
</form-login-config>
login.jsp:
if ("1".equals(request.getParameter("error"))) {
request.setAttribute("login_error", true);
} else {
// The initial render of the login page
String uuid;
String username;
// Form fields have priority over the persistent cookie
username = request.getParameter("j_username");
if (!isBlank(username)) {
String password = request.getParameter("j_password");
// set the cookie even though login may fail
// Will delete it later
if ("on".equals(request.getParameter("remember_me"))) {
uuid = UUID.randomUUID().toString();
addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE); // Extends age.
Map.Entry<String,String> creds =
new AbstractMap.SimpleEntry<String,String>(username,password);
rememberMeServiceSave(request, uuid, creds);
}
if (jSecurityCheck(request, response, username, password)) {
return;
}
request.setAttribute("login_error", true);
}
uuid = getCookieValue(request, COOKIE_NAME);
if (uuid != null) {
Map.Entry<String,String> creds = rememberMeServiceFind(request, uuid);
if (creds != null) {
username = creds.getKey();
String password = creds.getValue();
if (jSecurityCheck(request, response, username, password)) {
return; // going to redirect here again if login error
}
request.setAttribute("login_error", true);
}
}
}
// login failed
removeCookie(response, COOKIE_NAME);
// continue rendering the login page...
Here's some explanation:
Instead of calling request.login() we establish a new TCP connection to our HTTP listener and post the login form to the /j_security_check address. This allows the container to redirect us to the initially requested web page and restore the POST data (if any). Trying to obtain this info from a session attribute or RequestDispatcher.FORWARD_SERVLET_PATH would be container-specific.
We don't use a servlet filter for automatic login, because containers forward/redirect to the login page BEFORE the filter is reached.
The dynamic login page does all the job, including:
actually rendering the login form
accepting the filled form
calling /j_security_check under the hood
displaying login errors
automatic login
redirecting back to the initially requested page
To implement the "Stay Logged In" feature we save the credentials from the submitted login form in the servlet context attribute (for now). Unlike in the SO answer above, the password is not hashed, because only certain setups accept that (Glassfish with a jdbc realm). The persistent cookie is associated with the credentials.
The flow is the following:
Get forwarded/redirected to the login form
If we're served as the <form-error-page> then render the form and the error message
Otherwise, if some credentials are submitted, then store them and call /j_security_check and redirect to the outcome (which might be us again)
Otherwise, if the cookie is found, then retrieve the associated credentials and continue with /j_security_check
If none of the above, then render the login form without the error message
The code for /j_security_check sends a POST request using the current JSESSIONID cookie and the credentials either from the real form or associated with the persistent cookie.

eve U.R.R.A. doesn't work with HMAC

run.py:
class HMACAuth(HMACAuth):
def check_auth(self, userid, hmac_hash, headers, data, allowed_roles, resource, method):
accounts = app.data.driver.db['accounts']
user = accounts.find_one({'username': userid})
if user and '_id' in user:
secret_key = user['secret_key']
self.set_request_auth_value(user['_id'])
# in this implementation we only hash request data, ignoring the headers.
hm = hmac.new(bytes(secret_key, encoding='utf-8'), data, sha1).digest()
return user and base64.b64encode(hm).decode() == hmac_hash
settings.py:
vms = {
'additional_lookup': {
'url': 'regex("[\w]+")',
'field': 'name',
},
'cache_control': '',
'cache_expires': 0,
'public_methods': [],
'public_item_methods': [],
'resource_methods': ['GET', 'POST'],
'item_methods': ['GET','PATCH','DELETE'],
'auth_field': 'user_id',
'schema': vm_schema,
}
my problem is that every user is receiving all the VMs info when he/she send a GET request to localhost:5000/vms.
With the TokenAuth authentication this didn't happen.What am I missing??
PS: Eve 0.5-dev on Python 3.3.5
Since everything was working fine with token based authentication, and since there's nothing really different between the two methods expect the custom class itself, I would investigate around its behavior.
I would start by checking if documents are actually being stored with the proper user_id value, maybe by using the mongo shell. If not, make sure that the documents that you are inspecting have been saved with your custom HMAC class active. Add a breakpoint and track your code, simple stuff like that. Hope this helps

Redirect works incorrectly

I try to reidrect user to login page if he not authenticated, but redirect work in both cases. Here is my code:
$securityContext = $this->get('security.context');
if ($securityContext->isGranted('IS_AUTHENTICATED_ANONYMOUSLY')) {
return $this->redirect($this->generateUrl('fos_user_security_login'));
} else {
$currentUser = $securityContext->getToken()->getUser()->getProfile()->getId();
$pathToImages = $this->get('kernel')->getRootDir().'/../web';
$userCars = $this->getDoctrine()->getRepository('VputiUserBundle:Car')
->findBy(array('profile' => $currentUser));
return $this->render('VputiUserBundle:Car:index.html.twig',
array('userCars' =>$userCars,
'pathToImages' => $pathToImages));
}
Where I am wrong? Thanks)
Every user is granted IS_AUTHENTICATED_ANONYMOUSLY, so that will always resolve to true. What you want is to check that the user wasn't granted IS_AUTHENTICATED_REMEMBERED, like this:
if ( ! $securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
return $this->redirect($this->generateUrl('fos_user_security_login'));
} else {
...
}
I recommend reading this fantastic answer about roles for more information.
Anyway, in this case you don't need to do that. By default, when you send someone to a page that's behind a firewall it will automatically redirect them to the login page if they're not authenticated.
In Symfony2 are three level of authentication
IS_AUTHENTICATED_ANONYMOUSLY
IS_AUTHENTICATED_REMEMBERED
IS_AUTHENTICATED_FULLY
If a user have IS_AUTHENTICATED_FULLY role has also IS_AUTHENTICATED_REMEMBERED and IS_AUTHENTICATED_ANONYMOUSLY.
If a user have IS_AUTHENTICATED_REMEMBERED role has also IS_AUTHENTICATED_ANONYMOUSLY
So, everytime you check, you will pass that control. Try to "invert" logic: check for IS_AUTHENTICATED_FULLY and render the page, redirect otherwise
Working code for your case
$securityContext = $this->get('security.context');
if ($securityContext->isGranted('IS_AUTHENTICATED_FULLY')) {
$currentUser = $securityContext->getToken()->getUser()->getProfile()->getId();
$pathToImages = $this->get('kernel')->getRootDir().'/../web';
$userCars = $this->getDoctrine()->getRepository('VputiUserBundle:Car')
->findBy(array('profile' => $currentUser));
return $this->render('VputiUserBundle:Car:index.html.twig',
array('userCars' =>$userCars,
'pathToImages' => $pathToImages));
} else {
return $this->redirect($this->generateUrl('fos_user_security_login'));
}
Your problem is that all users who have been authenticated at all, either anonymously, via the "remember me" function, or fully using a username and password, have the "IS_AUTHENTICATED_ANONYMOUSLY" role; they stack. So your test always returns true, and the user is always redirected.
In this case, it may be better to do the test the other way around - redirect if the user doesn't have "IS_AUTHENTICATED_FULLY" or "IS_AUTHENTICATED_REMEMBERED".
See this related answer: https://stackoverflow.com/a/12984413/84649

Get GitHub avatar from email or name

I'm trying to get the GitHub user picture (avatar) from users of GitHub.
I've found these API:
https://avatars.githubusercontent.com/<username>
https://avatars.githubusercontent.com/u/<userid>
But I can't find a way to get the avatar from the user email or the user display name.
I can't find documentation about that.
Is there some similar URL API to get what I'm looking for?
You can append .png to the URL for the User's profile to get redirected to their avatar. You can add the query param size to specify a size smaller than the default of 460px wide (i.e. it won't allow larger than 460).
Examples:
https://github.com/twbs.png
https://github.com/npm.png?size=200
https://github.com/github.png?size=40
https://developer.github.com/v3/users/#get-a-single-user
Use the /users/:user endpoint. Should be under avatar_url in the returned json.
For example, my avatar_url can be found by hitting this url.
Edit
There is another way I can think of that is kind of roundabout. Since GitHub uses Gravatar, if you know the email associated with the account, do an md5 hash of the lowercase, stripped email address and construct a url like http://www.gravatar.com/avatar/[md5_here].
This is an old post but nobody has proposed Github Search Users API with scope field :
using in:email : https://api.github.com/search/users?q=bmartel+in%3Aemail
using in:username : https://api.github.com/search/users?q=Bertrand+Martel+in%3Ausername
Or using new Graphql API v4 :
{
search(type: USER, query: "in:email bmartel", first: 1) {
userCount
edges {
node {
... on User {
avatarUrl
}
}
}
}
}
Using GraphQL API v4, this will work too
Query (for username)-
{
user(login: "username") {
avatarUrl
}
}
Response -
{
"data": {
"user": {
"avatarUrl": "https://avatars1.githubusercontent.com/u/..."
}
}
}
GitHub avatar can be accessed through https://avatars.githubusercontent.com/u/YOUR_USER_ID
Optionally, you can modify the size at the end like so https://avatars.githubusercontent.com/u/YOUR_USER_ID?s=460

Resources