2
votes

Can't seem to create a functional way to insert a user from Java for Devise. Currently there are these fields: "_id", "access_level", "confirmation_sent_at", "confirmation_token", "confirmed_at", "email", "encrypted_password", "sign_in_count"

I am able to insert a document that counts as a user. The problem is that when I go to: http://www.mysite.com:3000/users/confirmation?confirmation_token=TOKENHERE

I get a message saying that it's invalid.

EDIT 1: When I resend confirmation instructions for this user (WHICH GENERATES A NEW TOKEN), the user can be logged into. This confirms my doubts about the token being the problem. How can I port Devise's token generator to Java?

EDIT 2: When I register on site, it says I should check for a confirmation link. However, if I go into the Mongo shell, manually take out the confirmation token and paste it to site.com/users/confirmation?confirmation_token= then it doesn't work! However, if I actually use the confirmation link I was sent, it works. How can I make a VALID token, all from Java. Please help!

2

2 Answers

0
votes

For this quoestion you should refer to this stackoverflow answer and to the Rails API of protect_from_forgery.

The short answer is to disable forgery protection in your controller, but this makes your application vulnerable to CSRF attacks:

skip_before_action :verify_authenticity_token

The better way would be to authenticate with a JSON or XML request as these requests are not protected by CSRF protection. You can find a solution for devise here.

Edit

Monkey patch devise to save unencoded confirmation token. In your config/initializers/devise.rb

module Devise
  module Models
    module Confirmable
      def generate_confirmation_token
        raw, enc = Devise.token_generator.generate(self.class, :confirmation_token)
        @raw_confirmation_token = raw
        self.my_unencoded_column = raw # Patch
        self.confirmation_token = enc
        self.confirmation_sent_at = Time.now.utc
      end
    end
  end
end
0
votes

In case anyone else finds themselves trying to get a java or scala app to coexist with a rails app, I hacked up the following. Its in scala but uses java apis so should be easy to read. As far as I can tell it replicates Devise's behavior, and if I hit the confirmation link in the rails app with the raw token rails/devise generates the same encoded string.

import java.security.spec.KeySpec
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
import javax.crypto.Mac
import javax.xml.bind.DatatypeConverter
import java.util.Base64 

// copy functionality from Rails Devise
object TokenGenerator {

  // sample values 9exithzwZ8P9meqdVs3K => 54364224169895883e87c8412be5874039b470e26e762cb3ddc37c0bdcf014f5
  //              5zNMi6egbyPoDUy2t3NY => 75bd5d53aa36d3fc61ac186b4c6e2be8353e6b39536d3cf846719284e05474ca

  private val deviseSecret = sys.env("DEVISE_SECRET")
  private val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
  val encoder = Base64.getUrlEncoder()

  case class TokenInfo(raw: String, encoded: String)

  def createConfirmationToken: TokenInfo = {
    // copy behavior from rails world. Don't know why it does this
    val replacements = Map('l' -> "s", 'I' -> "x", 'O' -> "y", '0' -> "z")

    // make a raw key of 20 chars, doesn't seem to matter what they are, just need url valid set
    val bytes = new Array[Byte](16)
    scala.util.Random.nextBytes(bytes)
    val raw = encoder.encodeToString(bytes).take(20).foldLeft(""){(acc, x) => acc ++ replacements.get(x).getOrElse(x.toString)}
    TokenInfo(raw, digestForConfirmationToken(raw))
  }

  private def generateKey(salt: String): Array[Byte] = {
    val iter = 65536
    val keySize = 512
    val spec = new PBEKeySpec(deviseSecret.toCharArray, salt.getBytes("UTF-8"), iter, keySize)
    val sk = factory.generateSecret(spec)
    val skspec = new SecretKeySpec(sk.getEncoded, "AES")
    skspec.getEncoded
  }

  def sha256HexDigest(s: String, key: Array[Byte]): String = {
    val mac = Mac.getInstance("HmacSHA256")
    val keySpec = new SecretKeySpec(key, "RAW")
    mac.init(keySpec)
    val result: Array[Byte] = mac.doFinal(s.getBytes())
    DatatypeConverter.printHexBinary(result).toLowerCase
  }

  private def getDigest(raw: String, salt: String) = sha256HexDigest(raw, generateKey(salt))

  // devise uses salt "Devise #{column}", in this case its confirmation_token
  def digestForConfirmationToken(raw: String) = getDigest(raw, "Devise confirmation_token")
}