Volvocars API (code from nikagl) - unauthorized_client

Hi,

I am using the the code from nikagl to get my volvo’s battery state and some releated data from the volvocars API. Unfortunately since a couple of days the authentication fails. I did check the [POSTMAN from Andy Nash](https://www.postman.com/andynash/volvo-apis/collection/3qx8sng/volvo-cars-apis) and discovered that an parameter has been added “Access Token Manager ID” which seem to be static, as you will find it as well in the evcc-io code. I added the new parameter, even though the authentication fails.

Is there anyone out there using the api and experience the same or able to help out?

Thanks in advance
Markus

Hi @magic3arkus ,

I am nikagl :wink: and use the code every day.:+1:. I haven’t observed this yet, but will investigate what is going on. Have you looked at the API spec (@ Volvo) to see whether something changed?

Regards,

Nika.

Hi @nikagl ,

good to have you watching it. Unfortunately I didn’t check the API docs/json, did it only on Andy’s postman code. I assume that the problem started when I need to get a new token instead of renewing it. You may want to test it with a arbitrary test application. For now I stopped investigating, because even with evcc I did not have success with there implementation.

Would be great to hear from you how things are evolving.

Best Markus

everyone you ever meet will know something you don’t

Volvo APIs haven’t changed since the last time I looked at them (last april).

It does look like there is more people running into this and many other topics. I think the main cause is that Volvo now requires OTP/2FA for authentication. Not related to the Access Token Manager ID, that seems to be not required (read that in other forums). The following is a script that allows requesting the OTP, but not sure yet how to integrate something like that into OpenHAB…

Maybe @rlkoshak can help. How can we setup something in OpenHAB so it does a 2FA, ie. runs a script then asks the user for the OTP code (sent to another device) and after returning the ID it generates the token that can be used in subsequent scripts?

I can’t be of much help. I didn’t do add-on developments and most of the services in aware of that have TFA also provide a way to generate an app password that bypasses TFA.

If something like this we’re built into the add-on, it world have to be rewritten in Java because you cannot guarantee Python will be available on every OH machine.

Thanks Rich. I am already quite far but it’s still a work in progress.

I have some items:

String volvootp "Volvo OTP" (volvo) ["Point"]
Switch VOC_Start_OTP "VOC Start OTP" (volvo) ["Switch"]
Switch VOC_Verify_OTP "VOC Verify OTP" (volvo) ["Switch"]

And some rules:

rule "VOC Start OTP"
when
    // Trigger the rule manually or based on some condition
    Item VOC_Start_OTP changed to ON
then
    // Step 1: Initial GET request for authorization
    var authURL = "https://volvoid.eu.volvocars.com/as/authorization.oauth2"
    var authHeaders = newHashMap()
    var authContent = "?client_id=h4Yf0b&response_type=code&response_mode=pi.flow"
    var authResponse = sendHttpGetRequest(authURL + authContent, authHeaders, 10000)

    if (authResponse !== null && authResponse.contains("USERNAME_PASSWORD_REQUIRED")) {
        logInfo("VOC", "Authorization succes, needs username & password")
        val username = volvousername.state.toString()
        val password = volvopassword.state.toString()

        // Step 2: POST username and password
        val checkUsernameUrl = transform("JSONPATH", "$._links.checkUsernamePassword.href", authResponse) + "?action=checkUsernamePassword"
		var authContentType = "application/json"
        
        authContent = '{"username": "' + username + '", "password": "' + password + '"}'
        authHeaders = newHashMap(
            "authorization" -> "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
            "user-agent" -> "okhttp/4.10.0",
            "Accept-Encoding" -> "gzip",
            "Content-Type" -> "application/json; charset=utf-8",
            "x-xsrf-header" -> "PingFederate"
        )

        val authPostResponse = sendHttpPostRequest(checkUsernameUrl, authContentType, authContent, authHeaders, 10000)
        if (authPostResponse !== null && authPostResponse.contains("OTP_REQUIRED")) {
            logInfo("VOC", "Authorization succes, needs OTP")
            // Step 3: OTP input
            val otpDevice = transform("JSONPATH", "$.devices[0].type", authPostResponse)
            val otpTarget = transform("JSONPATH", "$.devices[0].target", authPostResponse)
            logInfo("VOC", "OTP sent to " + otpDevice + " (" + otpTarget + ")")

            // Toggle VOC_Start_OTP item to show the rule finished succesfully
            VOC_Start_OTP.postUpdate(OFF)
        } else {
            logError("VOC", "Username/password authentication failed")
        }
    } else if (authResponse !== null && authResponse.contains("COMPLETED")) {
        logError("VOC", "Authorization was already completed")
            // Toggle VOC_Start_OTP item to show the rule finished succesfully
            VOC_Start_OTP.postUpdate(OFF)
    } else {
        logError("VOC", "Initial authorization failed")
    }
end

rule "VOC Verify OTP"
when
    // Trigger the rule manually or based on some condition
    Item VOC_Verify_OTP changed to ON
then
    val OAUTH_TOKEN_URL = "https://volvoid.eu.volvocars.com/as/token.oauth2"
    
    // Headers for the requests
    val headers = newHashMap(
        "authorization" -> "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
        "user-agent" -> "okhttp/4.10.0",
        "Accept-Encoding" -> "gzip",
        "Content-Type" -> "application/json; charset=utf-8"
    )

    // Assuming OTP is input manually in OpenHAB UI through an item
    val otp = volvootp.state.toString  // Replace with appropriate method for user input

    val checkOtpUrl = transform("JSONPATH", "$._links.checkOtp.href", authPostResponse) + "?action=checkOtp"
    val otpBody = '{"otp": "' + otp + '"}'

    val otpPostResponse = sendHttpPostRequest(checkOtpUrl, otpBody, "application/json", headers, 10000)
    if (otpPostResponse !== null && otpPostResponse.contains("OTP_VERIFIED")) {
        // Step 4: Continue authentication
        val continueUrl = transform("JSONPATH", "$._links.continueAuthentication.href", otpPostResponse) + "?action=continueAuthentication"
        val continueResponse = sendHttpGetRequest(continueUrl, headers, 10000)

        if (continueResponse !== null) {
            // Step 5: Get token
            val code = transform("JSONPATH", "$.authorizeResponse.code", continueResponse)
            val tokenBody = "code=" + code + "&grant_type=authorization_code"
            headers.put("Content-Type", "application/x-www-form-urlencoded")

            val tokenResponse = sendHttpPostRequest(OAUTH_TOKEN_URL, tokenBody, "application/x-www-form-urlencoded", headers, 10000)

            if (tokenResponse !== null) {
                // Store token in a .ini format or OpenHAB item
                val accessToken = transform("JSONPATH", "$.access_token", tokenResponse)
                val refreshToken = transform("JSONPATH", "$.refresh_token", tokenResponse)
                val expiresIn = transform("JSONPATH", "$.expires_in", tokenResponse)

                logInfo("VOC", "Token retrieved: " + accessToken)
                logInfo("VOC", "Refresh token: " + refreshToken)
                logInfo("VOC", "Expires in: " + expiresIn)
                // Optionally write to OpenHAB items or a file
                // Use persistence or scripting to store the token
            } else {
                logError("VOC", "Error fetching token")
            }
        } else {
            logError("VOC", "Error in continue authentication")
        }
    } else {
        logError("VOC", "OTP verification failed")
    }
end

By setting the VOC_Start_OTP to ON it will start initial OTP authentication (ie. send the mail to the OTP mailaddress which contains the OTP value)

After receiving the mail the OTP value needs to be added to the volvootp item.

As soon as it is set you can toggle the VOC_Verify_OTP which should start the actual verification.

Then it should save them to the accessToken item which is already part of the other integration, but still working on that part…

I’m a little stuck because authentication was already performed for my account when testing from postman and while setting up the code. For some reason it finds the authentication, I think from the cookies for the sendHttpGetRequest. In postman I know how to delete the cookies (and I can restart the whole authentication), but I cannot find a way to do that in OpenHAB…

Here’s my current progress:

rule "VOC Start OTP"
when
    Item VOC_Start_OTP changed to ON
then
    // Variable to incicate whether authentication is completed
    var completed = false

    // Step 1: Initial GET request for authorization
    var authURL = "https://volvoid.eu.volvocars.com/as/authorization.oauth2?client_id=h4Yf0b&response_type=code&response_mode=pi.flow" // no acr_values or scope required for initial request
    var authHeaders = newHashMap() // no headers required for initial request
    var authResponse = sendHttpGetRequest(authURL, authHeaders, 10000)
    logInfo("VOC", "Authorization response: " + authResponse)

// Auth already completed
//
// {
//     "id": "Do3CWHgNZE",
//     "pluginTypeId": "7RmQNDWaOnBoudTufx2sEw",
//     "status": "COMPLETED",
//     "authorizeResponse": {
//         "code": "kVCqC3kk8GQicmnN2RP3EQBuDKaFbH3yqpVgwv0v"
//     },
//     "user": {
//         "id": "83431904-5b4b-42af-9c95-e7deeb840f75",
//         "username": "*****obfuscated*****"
//     },
//     "_links": {
//         "self": {
//             "href": "https://volvoid.eu.volvocars.com/pf-ws/authn/flows/Do3CWHgNZE"
//         }
//     }
// }

// Auth Username Password required
//
// {
//     "id": "yYI6qv5k0A",
//     "pluginTypeId": "7RmQNDWaOnBoudTufx2sEw",
//     "status": "USERNAME_PASSWORD_REQUIRED",
//     "showRememberMyUsername": false,
//     "showThisIsMyDevice": false,
//     "thisIsMyDeviceSelected": false,
//     "showCaptcha": false,
//     "rememberMyUsernameSelected": false,
//     "_links": {
//         "self": {
//             "href": "https://volvoid.eu.volvocars.com/pf-ws/authn/flows/yYI6qv5k0A"
//         },
//         "checkUsernamePassword": {
//             "href": "https://volvoid.eu.volvocars.com/pf-ws/authn/flows/yYI6qv5k0A"
//         }
//     }
// }

    var authStatus = transform("JSONPATH", "$.status", authResponse)
    if (authStatus == "USERNAME_PASSWORD_REQUIRED") {
        logInfo("VOC", "Authorization succes, needs username & password")
        val username = volvousername.state.toString()
        val password = volvopassword.state.toString()

        // Step 2: POST username and password
        val checkUsernameUrl = transform("JSONPATH", "$._links.checkUsernamePassword.href", authResponse) + "?action=checkUsernamePassword"
        logInfo("VOC", "Authorization post URL: " + checkUsernameUrl)
		var checkUsernameContentType = "application/json"
        val checkUsernameContent = '{"username": "' + username + '", "password": "' + password + '"}'
        val checkUsernameHeaders = newHashMap(
            "x-xsrf-header" -> "PingFederate"
        )
        val authPostResponse = sendHttpPostRequest(checkUsernameUrl, checkUsernameContentType, checkUsernameContent, checkUsernameHeaders, 10000)
        logInfo("VOC", "Authorization post response: " + authPostResponse)

// Auth already completed
//
// {
// 	"id": "Dfs09s89o4",
// 	"pluginTypeId": "7RmQNDWaOnBoudTufx2sEw",
// 	"status": "COMPLETED",
// 	"authorizeResponse": {
// 		"code": "1jnA1QklBCTDTGvWufj1uKn39Gwj-QstrqgqxEKB"
// 	},
// 	"user": {
// 		"id": "83431904-5b4b-42af-9c95-e7deeb840f75",
// 		"username": "*****obfuscated*****"
// 	},
// 	"_links": {
// 		"self": {
// 			"href": "https://volvoid.eu.volvocars.com/pf-ws/authn/flows/Dfs09s89o4"
// 		}
// 	}
// }

// OTP required
//
// {
//     "id": "Zrre4SipNk",
//     "pluginTypeId": "7r5wkzvoQS8iEJEpdMYqmA",
//     "status": "OTP_REQUIRED",
//     "devices": [
//         {
//             "id": "f9262bc1-27a0-3e1a-bc06-49835a679205",
//             "type": "EMAIL",
//             "target": "*****obfuscated*****"
//         }
//     ],
//     "user": {
//         "username": "*****obfuscated*****"
//     },
//     "selectedDeviceRef": {
//         "id": "f9262bc1-27a0-3e1a-bc06-49835a679205"
//     },
//     "_links": {
//         "cancelAuthentication": {
//             "href": "https://volvoid.eu.volvocars.com/pf-ws/authn/flows/Zrre4SipNk"
//         },
//         "resendOtp": {
//             "href": "https://volvoid.eu.volvocars.com/pf-ws/authn/flows/Zrre4SipNk"
//         },
//         "selectDevice": {
//             "href": "https://volvoid.eu.volvocars.com/pf-ws/authn/flows/Zrre4SipNk"
//         },
//         "self": {
//             "href": "https://volvoid.eu.volvocars.com/pf-ws/authn/flows/Zrre4SipNk"
//         },
//         "checkOtp": {
//             "href": "https://volvoid.eu.volvocars.com/pf-ws/authn/flows/Zrre4SipNk"
//         }
//     }
// }
        var authPostResponseStatus = transform("JSONPATH", "$.status", authPostResponse)
        if (authPostResponseStatus == "OTP_REQUIRED") {
            logInfo("VOC", "Authorization succes, needs OTP")
            // Step 3: OTP input
            val otpDevice = transform("JSONPATH", "$.devices[0].type", authPostResponse)
            val otpTarget = transform("JSONPATH", "$.devices[0].target", authPostResponse)
            volvootpresponse.postUpdate("OTP sent to " + otpDevice + " (" + otpTarget + ")")
            logInfo("VOC", "OTP sent to " + otpDevice + " (" + otpTarget + ")")

            // Toggle VOC_Start_OTP item to show the rule finished succesfully
            VOC_Start_OTP.postUpdate(OFF)
        } else if (authPostResponseStatus == "COMPLETED") {
            var completedUserName = transform("JSONPATH", "$.user.username", authPostResponse)
            val authCode = transform("JSONPATH", "$.authorizeResponse.code", authPostResponse)
            completed = true
        } else {
            logError("VOC", "Username/password authentication failed")
        }
    } else if (authStatus == "COMPLETED") {
        var completedUserName = transform("JSONPATH", "$.user.username", authResponse)
        val authCode = transform("JSONPATH", "$.authorizeResponse.code", authResponse)
        completed = true
    } else {
        logError("VOC", "Initial authorization failed")
        volvootpresponse.postUpdate("Initial authorization failed")
    }

    if (completed) {
        logInfo("VOC", "Authorization was already (for " + completedUserName + ") completed, re-setting tokens")
        logInfo("VOC", "Authorization code: " + authCode)
        
        val tokenUrl = "https://volvoid.eu.volvocars.com/as/token.oauth2"
        var tokenContentType = "application/x-www-form-urlencoded"
        val tokenContent = "grant_type=authorization_code&code=" + authCode
        val tokenHeaders = newHashMap(
            'Authorization' -> 'Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=',
            'User-Agent' -> 'okhttp/4.10.0',
            'x-xsrf-header' -> 'PingFederate',
            'Accept-Encoding' -> 'gzip'
        )
        val tokenResponse = sendHttpPostRequest(tokenUrl, tokenContentType, tokenContent, tokenHeaders, 10000)
        logInfo("VOC", "Token response: " + tokenResponse)

        if (tokenResponse !== null) {
            // Store token in a .ini format or OpenHAB item
            val accessToken = transform("JSONPATH", "$.access_token", tokenResponse)
            val refreshToken = transform("JSONPATH", "$.refresh_token", tokenResponse)
            val expiresIn = transform("JSONPATH", "$.expires_in", tokenResponse)

            logInfo("VOC", "Token retrieved: " + accessToken)
            logInfo("VOC", "Refresh token: " + refreshToken)
            logInfo("VOC", "Expires in: " + expiresIn)

            volvootpresponse.postUpdate("Token retrieved!")
            // Optionally write to OpenHAB items or a file
            // Use persistence or scripting to store the token
        } else {
            logError("VOC", "Error fetching token")
            volvootpresponse.postUpdate("Error fetching token")
        }

        // Toggle VOC_Start_OTP item to show the rule finished succesfully
        VOC_Start_OTP.postUpdate(OFF)
    }
end

rule "VOC Verify OTP"
when
    Item VOC_Verify_OTP changed to ON or
    Item volvootp received update
then    
    // Headers for the requests
    val headers = newHashMap(
        "authorization" -> "Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=",
        "user-agent" -> "okhttp/4.10.0",
        "Accept-Encoding" -> "gzip",
        "Content-Type" -> "application/json; charset=utf-8"
    )

    // Assuming OTP is input manually in OpenHAB UI through an item
    val otp = volvootp.state.toString  // Replace with appropriate method for user input

    val checkOtpUrl = transform("JSONPATH", "$._links.checkOtp.href", authPostResponse) + "?action=checkOtp"
    val otpBody = '{"otp": "' + otp + '"}'
    val otpPostResponse = sendHttpPostRequest(checkOtpUrl, otpBody, "application/json", headers, 10000)

// {
//     "id": "IBROnO7ZBU",
//     "pluginTypeId": "7r5wkzvoQS8iEJEpdMYqmA",
//     "status": "OTP_VERIFIED",
//     "_links": {
//         "self": {
//             "href": "https://volvoid.eu.volvocars.com/pf-ws/authn/flows/IBROnO7ZBU"
//         },
//         "continueAuthentication": {
//             "href": "https://volvoid.eu.volvocars.com/pf-ws/authn/flows/IBROnO7ZBU"
//         }
//     }
// }  

    if (otpPostResponse !== null && otpPostResponse.contains("OTP_VERIFIED")) {
        // Step 4: Continue authentication
        val continueUrl = transform("JSONPATH", "$._links.continueAuthentication.href", otpPostResponse) + "?action=continueAuthentication"
        val continueResponse = sendHttpGetRequest(continueUrl, headers, 10000)

// {
//     "id": "IBROnO7ZBU",
//     "pluginTypeId": "7r5wkzvoQS8iEJEpdMYqmA",
//     "status": "COMPLETED",
//     "authorizeResponse": {
//         "code": "yUGn39NYMX9SQvhYs0aP_2V4GsWlBtEZF71gwv0v"
//     },
//     "user": {
//         "id": "83431904-5b4b-42af-9c95-e7deeb840f75",
//         "username": "*****obfuscated*****"
//     },
//     "_links": {
//         "self": {
//             "href": "https://volvoid.eu.volvocars.com/pf-ws/authn/flows/IBROnO7ZBU"
//         }
//     }
// }

        if (continueResponse !== null) {
            // Step 5: Get token
            val authCode = transform("JSONPATH", "$.authorizeResponse.code", continueResponse)
            logInfo("VOC", "Authorization code: " + authCode)
            
            val tokenUrl = "https://volvoid.eu.volvocars.com/as/token.oauth2"
            var tokenContentType = "application/x-www-form-urlencoded"
            val tokenContent = "grant_type=authorization_code&code=" + authCode
            val tokenHeaders = newHashMap(
                'Authorization' -> 'Basic aDRZZjBiOlU4WWtTYlZsNnh3c2c1WVFxWmZyZ1ZtSWFEcGhPc3kxUENhVXNpY1F0bzNUUjVrd2FKc2U0QVpkZ2ZJZmNMeXc=',
                'User-Agent' -> 'okhttp/4.10.0',
                'x-xsrf-header' -> 'PingFederate',
                'Accept-Encoding' -> 'gzip'
            )
            val tokenResponse = sendHttpPostRequest(tokenUrl, tokenContentType, tokenContent, tokenHeaders, 10000)
            logInfo("VOC", "Token response: " + tokenResponse)

            if (tokenResponse !== null) {
                // Store token in a .ini format or OpenHAB item
                val accessToken = transform("JSONPATH", "$.access_token", tokenResponse)
                val refreshToken = transform("JSONPATH", "$.refresh_token", tokenResponse)
                val expiresIn = transform("JSONPATH", "$.expires_in", tokenResponse)

                logInfo("VOC", "Token retrieved: " + accessToken)
                logInfo("VOC", "Refresh token: " + refreshToken)
                logInfo("VOC", "Expires in: " + expiresIn)

                volvootpresponse.postUpdate("Token retrieved!")
                // Optionally write to OpenHAB items or a file
                // Use persistence or scripting to store the token
            } else {
                logError("VOC", "Error fetching token")
                volvootpresponse.postUpdate("Error fetching token")
            }

            // Toggle VOC_Start_OTP item to show the rule finished succesfully
            VOC_Verify_OTP.postUpdate(OFF)
        } else {
            logError("VOC", "Error in continue authentication")
        }
    } else {
        logError("VOC", "OTP verification failed")
    }
end

I still cannot find a way to delete the cookies and rerun authentication. Anyone know how to delete cookies for the requests running with sendHttpGetRequest or sendHttpPostRequest?

In my case it returns an auth token succesfully, but when i try to use it, it complains about 403
/ FORBIDDEN / Access Denied:

HTTP Error vccResponse = {
“status” : 403,
“operationId” : “65766437-3087-41cb-afb3-da50ecc60691”,
“error” : {
“message” : “FORBIDDEN”,
“description” : “Access Denied”
}
}

Hi Nika, it might be a good idea to open another thread to ask about the cookie deletion. I doubt that in this thread there is enough attention :wink:

Appreciate all Your effort though!

Ok, so, it is still a bit “quick and dirty”. I have replaced the code in the function that authenticates (authenticateVolvo) by a function that requests the OTP. If it cannot authenticate yet (because it needs the OTP to be entered) updating the volvo stats will simply fail. I have an item in the list that receives information about the otp, volvootpresponse, which will show “OTP sent to XXX (YYY)”. The XXX is the device you setup for OTP at Volvo, and the YYY would be the mailaddress for instance. You can add that item (volvootpresponse) to a page to see what the status is. Other messages might be:

Initial authorization failed
Error fetching token
Token retrieved!
Error fetching token
Error in continue authentication
OTP verification failed

It also saves the ID which accompanied getting the OTP, so it can be used later when the OTP is entered. As soon as you see “OTP sent to XXX (YYY)” you need to check your device and can enter the received OTP in the volvootp item. As soon as you update this item (or flick the switch in the VOC_Verify_OTP item) it will use the OTP and get the access_token.

After getting the token it will use that acces_token in all volvo calls (and it will work :slight_smile:) until it expires and as soon as it expires, it will request another access_token with the existing ID. As far as I have observed it works fine like that. I have not (yet) seen a way to use the refresh_token and such, will need to investigate a little more on how it works for other home automation tools (domoticz and such) as it seems they have more (or better :upside_down_face:) people developing for those tools…

Hope it works for you… let me know if you have any questions or see errors (as I had to strip a little of the code because of my PII and some custom code in it).

Regards,

Nika.

I have updated everything on my repo and all has been working fine for at least two weeks now :+1:

2 Likes

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.