Open othrif opened 9 months ago
it looks like BrightHorizons has changed their login flow - the POST to the url in the code is no longer a valid way to authenticate. I will see if I can figure out the new flow.
Happy to assist in the debugging if there is anything I can help with. On 19. Jan 2024, at 13:07, Leo Covarrubias @.***> wrote: it looks like BrightHorizons has changed their login flow - the POST to the url in the code is no longer a valid way to authenticate. I will see if I can figure out the new flow.
—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you authored the thread.Message ID: @.***>
when I try to visit:
https://familyinfocenter.brighthorizons.com/mybrightday/login
i get redirected to:
https://bhlogin.brighthorizons.com?benefitid=5&fstargetid=1
the login form at this new url (as far as i can tell not being able to actually log in) posts to itself with a more complex request including a ephemeral verification token like so:
POST https://bhlogin.brighthorizons.com?benefitid=5&fstargetid=1
Content-Type: application/x-www-form-urlencoded
Form data:
__RequestVerificationToken=<random token>
benefitId=5
clientGuid=<not sure if this needs a value>
userType=<not sure if this needs a value>
fsTargetId=1
returnURL=<not sure if this needs a value>
JsonDfp=<json string describing the host machine and browser>
username=<username>
password=<password>
not sure how we would be able to generate the __RequestVerificationToken
needs more digging.
the benefitId
and fsTargetId
seem to indicate the target portal for a unified login system for BH's different services.
the clientGuid
value probably indicates something about the host like if the request is from the mobile app.
in the repository the file api_docs/bright_horizons.http
describes most of the requests the code tries to make. My main concern is that if they've changed the login flow they may have changed more about their API.
Indeed, it seems the login process of BH involves multiple steps, including redirections. From googling, the RequestVerificationToken is an anti-forgery token used to prevent CSRF attacks, and it is usually generated by the server and must be included in a subsequent POST request. The` RequestVerificationToken` can't be generated client-side; it must be issued by the server and is tied to the user's session.
From what i can see, when we visit https://familyinfocenter.brighthorizons.com/mybrightday/login, we are redirected to a centralized login system at https://bhlogin.brighthorizons.com. This system handles authentication for various Bright Horizons services and requires a benefitId and fsTargetId to route the login process correctly.
Upon submitting my login credentials, a POST request is made to https://bhlogin.brighthorizons.com, which includes __RequestVerificationToken
alongside the benefitId
, fsTargetId
, and user credentials.
The browser is directed back to the Family Information Center https://familyinfocenter.brighthorizons.com/welcome/login, maybe indicating the completion of the authentication process.
As I see in the request named login
, here is the request:
curl 'https://familyinfocenter.brighthorizons.com/welcome/login' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \
-H 'Accept-Language: en' \
-H 'Cache-Control: no-cache' \
-H 'Connection: keep-alive' \
-H 'Cookie: <redacted>' \
-H 'Pragma: no-cache' \
-H 'Referer: https://bhlogin.brighthorizons.com/' \
-H 'Sec-Fetch-Dest: document' \
-H 'Sec-Fetch-Mode: navigate' \
-H 'Sec-Fetch-Site: same-site' \
-H 'Upgrade-Insecure-Requests: 1' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' \
-H 'sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'sec-ch-ua-platform: "macOS"' \
--compressed
Let me know what i can check next.
the __RequestVerificationToken
is available in the initial html as a hidden form field it may be possible to do something like:
__RequestVerificationToken
and store the session cookiesat this point on being redirected to https://familyinfocenter.brighthorizons.com/welcome/login either the new cookies are used for api request validation or there is some javascript that fetches additional data to eventually get an api key.
you can check a browsers dev tools once fully logged in and inspect the requests that fetch data to see what is authenticating the api - either the previous X-Api-Key
header or something else (maybe only a cookie)
Extracting the __RequestVerificationToken
from the initial HTML response seems like a good way to proceed.
I see a Set-Cookie
header in the login
Response Headers. I presume this means the server may be setting a session cookie that is used for authentication.
GET /welcome/login HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en,en-US;q=0.9,fr-FR;q=0.8,fr;q=0.7,ar;q=0.6
Cache-Control: no-cache
Connection: keep-alive
**Cookie: <redacted>**
Host: familyinfocenter.brighthorizons.com
Pragma: no-cache
Referer: https://bhlogin.brighthorizons.com/
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-site
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
HTTP/1.1 200 OK
Content-Type: text/html
Content-Encoding: gzip
Last-Modified: Thu, 18 Jan 2024 09:52:06 GMT
Accept-Ranges: bytes
ETag: "0e7b4fef349da1:0"
Vary: Accept-Encoding
Server:
Access-Control-Allow-Origin: *
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000
Content-Security-Policy: frame-ancestors 'none'
Date: Sat, 20 Jan 2024 03:42:59 GMT
Content-Length: 3209
Strict-Transport-Security: max-age=157680000
**Set-Cookie: <redacted>**
Strict-Transport-Security: max-age=31536000
I am attaching a screenshot of the other requests that are happening if you want to see more details about one of them.
It's going to be very difficult for me to reverse engineer the login flow as it stands.
If you are comfortable with Python I've started a script attempting to execute the login flow in this branch: https://github.com/leocov-dev/tadpoles-backup/pull/49/files
executed as:
$ python api_docs/bright_horizons_login_flow.py <username> <password>
Perhaps you are able to make progress with this? As a side note it seems that 4 invalid login attempts triggers an account lock (seems they probably send an email to unlock).
Important indicators you can reference after login in Chrome dev tools network capture under Fetch/XHR
are:
/api/v2/auth/jwt/validate
/api/v2/user/profile
and did it contain an X-Api-Key
header or another header such as Authorization
I got this working today - after the CSRF hurdle, we also need to go through a SAML request, exchange the Bright Horizons token for a Tadpole token, and then we can exchange that for session cookies. Once those are available, I'm able to hit endpoints like /remote/v1/events
. There are three different hosts in the flow, so maybe there's a simpler path, but this mimics what the browser does. Different data comes from different hosts, so I hide that a little behind a generic get
method, but it'd surely be cleaner to pull things apart into multiple clients. At any rate, I hope this can help y'all port to the languages you want.
class Client
BRIGHT_HORIZONS_BASE = 'https://bhlogin.brighthorizons.com'
APIS = {
bright_horizons: 'https://mbdwgateway.brighthorizons.com/api',
tadpole: 'https://mybrightday.brighthorizons.com'
}
DEBUG = false
attr_reader :username, :password, :bh_token, :tadpole_token, :tadpole_cookies
def initialize(username, password)
@username = username
@password = password
end
def debug(*strs)
puts(*strs) if DEBUG
end
def log_in
bh_start_page = HTTP.get("#{BRIGHT_HORIZONS_BASE}/?benefitid=5&fstargetid=1")
verification_token = Nokogiri::HTML(bh_start_page.body.to_s).css('input[name="__RequestVerificationToken"]').first['value']
cookies = bh_start_page.cookies
login_response = HTTP.cookies(cookies).post(
"#{BRIGHT_HORIZONS_BASE}",
form: {
username: username,
password: password,
__RequestVerificationToken: verification_token,
benefitid: 5,
fstargetid: 1,
userType: 0
}
)
cookies = login_response.cookies
redirect = login_response.headers['Location']
bh_response = HTTP.cookies(cookies).get("#{BRIGHT_HORIZONS_BASE}#{redirect}")
html = Nokogiri::HTML(bh_response.body.to_s)
action = html.css('form').first['action']
saml_response = html.css('input[name="SAMLResponse"]').first['value']
finish_saml = HTTP.cookies(cookies).post(action, form: { SAMLResponse: saml_response })
cookies = finish_saml.cookies
@bh_token = cookies.find { |c| c.name == 'acs' }.value
token_response = HTTP.headers({ Authorization: "Bearer #{@bh_token}" }).get("#{APIS[:bright_horizons]}/account/token2")
@tadpole_token = token_response.parse(:json)['token']
tadpole_login = HTTP.get("#{APIS[:tadpole]}/auth/jwt/redirect", params: { jwt: @tadpole_token })
@tadpole_cookies = tadpole_login.cookies
nil
end
def children
@children ||= get(:bright_horizons, '/home/mychildren', {})['children']
end
def events(date)
debug "Fetching events from #{date.to_time.utc.to_i} to #{(date + 1).to_time.utc.to_i}"
page_size = 1000
data = get(
:tadpole,
'/remote/v1/events',
client: 'dashboard',
direction: 'range',
num_events: page_size, # They send this param but don't appear to use it
earliest_event_time: date.to_time.utc.to_i,
latest_event_time: (date + 1).to_time.utc.to_i
)
all_events = data['events']
debug "Found #{all_events.size} events"
all_events
end
def get(api, path, params)
token, cookies = api == :bright_horizons ? [bh_token, {}] : [tadpole_token, tadpole_cookies]
response = HTTP.cookies(cookies)
.get("#{APIS[api]}#{path}",
headers: { Authorization: "Bearer #{token}" },
params: params)
response.parse(:json)
end
end
Thank you @kristjan for figuring this out and providing an example! It should be enough for me to update the login flow and get a branch out for testing.
@kristjan after you get a tadpoles-token from calling https://mbdwgateway.brighthorizons.com/api//account/token2
are you able to call:
POST https://mybrightday.brighthorizons.com/api/v2/auth/jwt/validate
Content-Type: application/x-www-form-urlencoded
token=<tadpoles-token>
to get a bh-api-key and then call the v2 api endpoint such as:
GET https://mybrightday.brighthorizons.com/api/v2/user/profile
Accept: application/json
X-Api-Key: <bh-api-key>
I'd like to figure out how much modification i'll need to do besides the login flow.
I think they've replaced /jwt/validate
with /jwt/redirect
. /use/profile
returns Unauthorized
, but their site is currently throwing a 500 so I can't see what that might have been changed to 😬 I'll try to poke at again when they come back up.
Other mybrightday.brighthorizons.com
URLs like /remote/v1/events
are working.
I updated the branch: bright-horizons-new-login-flow / PR with changes to start to bring things in line with @kristjan ruby example.
I have no way to test this since I don't have a BH account so unless someone steps up to push this forward I think this is stuck. The contents of internal/api/bright_horizons/provider.go
is what needs to be figured out.
I started writing some tests for parts that parse HTML here: internal/api/bright_horizons/login_test.go
, but although these tests pass I don't know what the data actually looks like so it's not currently a valid test.
I tested it, and I am getting the following error: Cmd Error : error finding SAML response
More info:
DEBU[0003] Login...
DEBU[0004] Login successful, Admit...
Cmd Error : error finding SAML response
Even if the username/password is random characters
I debugged a little bit, and I think the problem is with the cookies.
Describe the bug When attempting to use the tadpoles-backup tool with the stat command to log into Bright Horizons, the process fails with a "405 - HTTP verb used to access this page is not allowed" error. This occurs during the login attempt to Bright Horizons, suggesting an issue with the HTTP method used for the POST request to /mybrightday/login.
Note that I used the same username (not email) and password I used to login to: https://familyinfocenter.brighthorizons.com/mybrightday/login successfully.
To Reproduce Steps to reproduce the behavior:
Expected behavior I expected the tadpoles-backup tool to successfully log into the Bright Horizons service and provide status information without encountering a server error.
Logs Logs from the console:
System Info (please complete the following information):
Arch: amd64
I used the latest
tadpoles-backup-darwin-amd64.zip
build from release v2.1.0